Skip to main content

camel_api/
ssrf.rs

1//! SSRF (Server-Side Request Forgery) defense helpers.
2//!
3//! Canonical IP-classification logic shared by every outbound HTTP client
4//! in the workspace. Centralising this prevents drift between crates
5//! (e.g. one allowing ULA, another not) and makes the rule set auditable
6//! in one place.
7//!
8//! Blocking policy:
9//! - IPv4: private, loopback, link-local, broadcast, multicast, unspecified, 0.0.0.0/8,
10//!   CGN (100.64.0.0/10), benchmark (198.18.0.0/15), reserved future-use (240.0.0.0/4)
11//! - IPv6: loopback, multicast, unspecified, ULA (fc00::/7), link-local (fe80::/10),
12//!   deprecated site-local (fec0::/10)
13//!
14//! Public, routable addresses always return `false`. Domain-name validation
15//! is the caller's responsibility — this helper operates on `IpAddr`.
16
17use std::net::IpAddr;
18
19/// SSRF validation policy for outbound HTTP clients.
20///
21/// `PublicHttpsOnly` is the default and enforces HTTPS + public IPs only.
22/// `AllowInternal` relaxes both: permits private/loopback IPs and permits
23/// HTTP scheme **only when all resolved IPs are internal**. Public IPs
24/// over HTTP remain blocked to prevent cleartext credentials to the internet.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26#[non_exhaustive]
27pub enum SsrfPolicy {
28    #[default]
29    PublicHttpsOnly,
30    AllowInternal,
31}
32
33impl SsrfPolicy {
34    /// Returns `true` when internal/private addresses are permitted.
35    pub fn allows_internal(self) -> bool {
36        matches!(self, Self::AllowInternal)
37    }
38}
39
40/// Returns `true` if `ip` belongs to a network that must NOT be reached
41/// by an outbound HTTP client in this workspace.
42///
43/// Blocks:
44///
45/// - **IPv4**: private (RFC 1918), loopback (127.0.0.0/8), link-local
46///   (169.254.0.0/16 — cloud metadata), broadcast (255.255.255.255),
47///   multicast (224.0.0.0/4), unspecified (0.0.0.0), the entire
48///   `0.0.0.0/8` block (first octet 0), carrier-grade NAT
49///   (100.64.0.0/10 — RFC 6598), network interconnect benchmark
50///   (198.18.0.0/15 — RFC 2544), and the reserved future-use
51///   `240.0.0.0/4` block (RFC 1112).
52/// - **IPv6**: loopback (::1), multicast (ff00::/8), unspecified (::),
53///   unique-local (fc00::/7), link-local (fe80::/10), and deprecated
54///   site-local (fec0::/10 — RFC 3879).
55///   IPv4-mapped IPv6 (`::ffff:a.b.c.d`) inherits the classification of
56///   the embedded IPv4 — required to close the DNS-rebinding bypass where
57///   a public AAAA record maps to a private IPv4 in v4-mapped form.
58///
59/// Public, routable addresses (e.g. 8.8.8.8, 2001:4860:4860::8888) return `false`.
60pub fn is_ssrf_blocked_ip(ip: &IpAddr) -> bool {
61    match ip {
62        IpAddr::V4(v4) => {
63            v4.is_private()
64                || v4.is_loopback()
65                || v4.is_link_local()
66                || v4.is_broadcast()
67                || v4.is_multicast()
68                || v4.is_unspecified()
69                || v4.octets()[0] == 0
70                // CGN 100.64.0.0/10 (RFC 6598) — carrier-grade NAT range,
71                // commonly used as shared address space inside ISPs and
72                // occasionally leaked to internal networks.
73                || (v4.octets()[0] == 100
74                    && (v4.octets()[1] >= 64 && v4.octets()[1] <= 127))
75                // Network interconnect benchmark 198.18.0.0/15 (RFC 2544) —
76                // reserved for benchmarking; never appears on the public
77                // internet, only on lab equipment that an attacker could
78                // pivot through.
79                || (v4.octets()[0] == 198
80                    && (v4.octets()[1] == 18 || v4.octets()[1] == 19))
81                // Reserved future-use 240.0.0.0/4 (RFC 1112) — covers
82                // 240.0.0.0..255.255.255.254. Currently unallocated;
83                // blocking prevents any surprise assignment from becoming
84                // an SSRF target.
85                || v4.octets()[0] >= 240
86        }
87        IpAddr::V6(v6) => {
88            v6.is_loopback()
89                || v6.is_multicast()
90                || v6.is_unspecified()
91                // ULA fc00::/7 — covers both fc00::/8 and fd00::/8
92                || (v6.segments()[0] & 0xfe00) == 0xfc00
93                // Link-local fe80::/10
94                || (v6.segments()[0] & 0xffc0) == 0xfe80
95                // Deprecated site-local fec0::/10 (RFC 3879). Replaced by
96                // ULA but still routable in some legacy networks.
97                || (v6.segments()[0] & 0xffc0) == 0xfec0
98                // IPv4-mapped IPv6: recurse into the embedded IPv4 to
99                // close the rebinding bypass.
100                || v6
101                    .to_ipv4_mapped()
102                    .map(|v4| {
103                        v4.is_private()
104                            || v4.is_loopback()
105                            || v4.is_link_local()
106                            || v4.is_broadcast()
107                            || v4.is_multicast()
108                            || v4.is_unspecified()
109                            || v4.octets()[0] == 0
110                            || (v4.octets()[0] == 100
111                                && (v4.octets()[1] >= 64 && v4.octets()[1] <= 127))
112                            || (v4.octets()[0] == 198
113                                && (v4.octets()[1] == 18 || v4.octets()[1] == 19))
114                            || v4.octets()[0] >= 240
115                    })
116                    .unwrap_or(false)
117                // NAT64 Well-Known Prefix 64:ff9b::/96 (RFC 6052): the last
118                // 32 bits are an embedded IPv4 — recurse so a NAT64 address
119                // translating an internal IPv4 is blocked (IPv6-only fabrics
120                // reach internal v4 through the NAT64 gateway).
121                || nat64_embedded_blocked(v6)
122                // 6to4 2002::/16 (RFC 3056, deprecated by RFC 7526): embeds
123                // the relay's IPv4 in bits 16..48. Blocked outright — the
124                // mechanism is deprecated and only ever tunnels, so a legit
125                // deployment never needs it as an HTTP target.
126                || v6.segments()[0] == 0x2002
127                // Teredo 2001::/32 (RFC 4380): tunnels IPv4 through NATs.
128                // Blocked for the same reason as 6to4.
129                || (v6.segments()[0] == 0x2001 && v6.segments()[1] == 0x0000)
130        }
131    }
132}
133
134/// NAT64 WKP `64:ff9b::/96` (RFC 6052): extract the embedded IPv4 from the
135/// last 32 bits and apply the IPv4 blocked-range classification.
136fn nat64_embedded_blocked(v6: &std::net::Ipv6Addr) -> bool {
137    let s = v6.segments();
138    // Prefix match on 64:ff9b::/96 (first six segments fixed).
139    if s[..6] != [0x0064, 0xff9b, 0, 0, 0, 0] {
140        return false;
141    }
142    let v4 = std::net::Ipv4Addr::new((s[6] >> 8) as u8, s[6] as u8, (s[7] >> 8) as u8, s[7] as u8);
143    is_ssrf_blocked_ip(&IpAddr::V4(v4))
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::net::Ipv4Addr;
150
151    fn v4(s: &str) -> IpAddr {
152        IpAddr::V4(s.parse::<Ipv4Addr>().expect("valid ipv4")) // allow-unwrap
153    }
154
155    fn v6(s: &str) -> IpAddr {
156        IpAddr::V6(s.parse().expect("valid ipv6")) // allow-unwrap
157    }
158
159    // ---- IPv4: blocked ranges ----
160
161    #[test]
162    fn blocks_rfc1918_10() {
163        assert!(is_ssrf_blocked_ip(&v4("10.0.0.1")));
164        assert!(is_ssrf_blocked_ip(&v4("10.255.255.255")));
165    }
166
167    #[test]
168    fn blocks_rfc1918_172_16() {
169        assert!(is_ssrf_blocked_ip(&v4("172.16.1.10")));
170        assert!(is_ssrf_blocked_ip(&v4("172.31.255.254")));
171    }
172
173    #[test]
174    fn blocks_rfc1918_192_168() {
175        assert!(is_ssrf_blocked_ip(&v4("192.168.1.1")));
176        assert!(is_ssrf_blocked_ip(&v4("192.168.0.0")));
177    }
178
179    #[test]
180    fn blocks_loopback_v4() {
181        assert!(is_ssrf_blocked_ip(&v4("127.0.0.1")));
182        assert!(is_ssrf_blocked_ip(&v4("127.255.255.254")));
183    }
184
185    #[test]
186    fn blocks_link_local_v4() {
187        // 169.254/16 — cloud metadata endpoints
188        assert!(is_ssrf_blocked_ip(&v4("169.254.169.254")));
189        assert!(is_ssrf_blocked_ip(&v4("169.254.1.1")));
190    }
191
192    #[test]
193    fn blocks_broadcast_v4() {
194        assert!(is_ssrf_blocked_ip(&v4("255.255.255.255")));
195    }
196
197    #[test]
198    fn blocks_multicast_v4() {
199        assert!(is_ssrf_blocked_ip(&v4("224.0.0.1")));
200        assert!(is_ssrf_blocked_ip(&v4("239.255.255.255")));
201    }
202
203    #[test]
204    fn blocks_unspecified_v4() {
205        assert!(is_ssrf_blocked_ip(&v4("0.0.0.0")));
206    }
207
208    #[test]
209    fn blocks_zero_octet_v4() {
210        // 0.0.0.0/8 — first octet 0, but not the unspecified address
211        assert!(is_ssrf_blocked_ip(&v4("0.1.2.3")));
212        assert!(is_ssrf_blocked_ip(&v4("0.255.255.255")));
213    }
214
215    #[test]
216    fn blocks_cgn_v4() {
217        // CGN 100.64.0.0/10 (RFC 6598) — first octet 100, second 64..=127
218        assert!(is_ssrf_blocked_ip(&v4("100.64.0.0")));
219        assert!(is_ssrf_blocked_ip(&v4("100.100.100.100")));
220        assert!(is_ssrf_blocked_ip(&v4("100.127.255.255")));
221        // 100.63 and 100.128 are NOT CGN
222        assert!(!is_ssrf_blocked_ip(&v4("100.63.255.255")));
223        assert!(!is_ssrf_blocked_ip(&v4("100.128.0.0")));
224    }
225
226    #[test]
227    fn blocks_benchmark_v4() {
228        // Benchmark 198.18.0.0/15 (RFC 2544) — second octet 18 or 19
229        assert!(is_ssrf_blocked_ip(&v4("198.18.0.0")));
230        assert!(is_ssrf_blocked_ip(&v4("198.18.255.255")));
231        assert!(is_ssrf_blocked_ip(&v4("198.19.255.255")));
232        // 198.17 and 198.20 are NOT benchmark
233        assert!(!is_ssrf_blocked_ip(&v4("198.17.255.255")));
234        assert!(!is_ssrf_blocked_ip(&v4("198.20.0.0")));
235    }
236
237    #[test]
238    fn blocks_reserved_v4() {
239        // Reserved 240.0.0.0/4 — first octet >= 240
240        assert!(is_ssrf_blocked_ip(&v4("240.0.0.0")));
241        assert!(is_ssrf_blocked_ip(&v4("241.1.2.3")));
242        assert!(is_ssrf_blocked_ip(&v4("250.100.200.50")));
243        // broadcast 255.255.255.255 already covered by is_broadcast
244        assert!(is_ssrf_blocked_ip(&v4("255.255.255.255")));
245        // 239.x is the top of multicast (224.0.0.0/4), NOT reserved —
246        // multicast is still blocked, but via a different rule.
247        assert!(is_ssrf_blocked_ip(&v4("239.255.255.255")));
248        // 100.x is CGN, blocked, not reserved
249        assert!(is_ssrf_blocked_ip(&v4("100.100.100.100")));
250    }
251
252    // ---- IPv4: allowed ranges ----
253
254    #[test]
255    fn allows_public_dns_v4() {
256        assert!(!is_ssrf_blocked_ip(&v4("8.8.8.8")));
257        assert!(!is_ssrf_blocked_ip(&v4("1.1.1.1")));
258    }
259
260    #[test]
261    fn allows_public_edge_v4() {
262        // 172.15 and 172.32 are NOT RFC-1918 (only 172.16/12 is)
263        assert!(!is_ssrf_blocked_ip(&v4("172.15.255.255")));
264        assert!(!is_ssrf_blocked_ip(&v4("172.32.0.0")));
265    }
266
267    // ---- IPv6: blocked ranges ----
268
269    #[test]
270    fn blocks_loopback_v6() {
271        assert!(is_ssrf_blocked_ip(&v6("::1")));
272    }
273
274    #[test]
275    fn blocks_unspecified_v6() {
276        assert!(is_ssrf_blocked_ip(&v6("::")));
277    }
278
279    #[test]
280    fn blocks_multicast_v6() {
281        assert!(is_ssrf_blocked_ip(&v6("ff02::1")));
282        assert!(is_ssrf_blocked_ip(&v6("ff00::1")));
283    }
284
285    #[test]
286    fn blocks_ula_fc_v6() {
287        assert!(is_ssrf_blocked_ip(&v6("fc00::1")));
288        assert!(is_ssrf_blocked_ip(&v6("fc00:1234:abcd::1")));
289    }
290
291    #[test]
292    fn blocks_ula_fd_v6() {
293        assert!(is_ssrf_blocked_ip(&v6("fd00::1")));
294        assert!(is_ssrf_blocked_ip(&v6("fd12:3456:789a::1")));
295    }
296
297    #[test]
298    fn blocks_link_local_v6() {
299        assert!(is_ssrf_blocked_ip(&v6("fe80::1")));
300        // fe80::/10 covers fe80..febf
301        assert!(is_ssrf_blocked_ip(&v6("febf:ffff::1")));
302        // febf + 1 is site-local, which is also blocked (separate test below)
303    }
304
305    #[test]
306    fn blocks_site_local_v6() {
307        // fec0::/10 (RFC 3879) — deprecated site-local, but still
308        // routable in some legacy networks.
309        assert!(is_ssrf_blocked_ip(&v6("fec0::1")));
310        assert!(is_ssrf_blocked_ip(&v6("feff:ffff::1")));
311        // febf is link-local, blocked by the fe80::/10 rule
312        assert!(is_ssrf_blocked_ip(&v6("febf::1")));
313        // ff00::/8 is multicast, already blocked
314        assert!(is_ssrf_blocked_ip(&v6("ff00::1")));
315    }
316
317    // ---- IPv6: allowed ranges ----
318
319    #[test]
320    fn allows_public_dns_v6() {
321        assert!(!is_ssrf_blocked_ip(&v6("2001:4860:4860::8888")));
322    }
323
324    #[test]
325    fn allows_public_documentation_v6() {
326        assert!(!is_ssrf_blocked_ip(&v6("2001:db8::1")));
327    }
328
329    // ---- Audit 2026-08-31, F2-6: transition-mechanism ranges ----
330
331    #[test]
332    fn blocks_nat64_embedding_private_ipv4() {
333        // 64:ff9b::0a00:0001 = NAT64 of 10.0.0.1
334        assert!(is_ssrf_blocked_ip(&v6("64:ff9b::a00:1")));
335        // 64:ff9b::7f00:0001 = NAT64 of 127.0.0.1
336        assert!(is_ssrf_blocked_ip(&v6("64:ff9b::7f00:1")));
337    }
338
339    #[test]
340    fn allows_nat64_embedding_public_ipv4() {
341        // 64:ff9b::0808:0808 = NAT64 of 8.8.8.8 (public)
342        assert!(!is_ssrf_blocked_ip(&v6("64:ff9b::808:808")));
343    }
344
345    #[test]
346    fn blocks_6to4_and_teredo() {
347        assert!(is_ssrf_blocked_ip(&v6("2002:0a00:0001::1"))); // 6to4 of 10.0.0.1
348        assert!(is_ssrf_blocked_ip(&v6("2002:0808:0808::1"))); // 6to4 even for public v4
349        assert!(is_ssrf_blocked_ip(&v6(
350            "2001:0000:4136:e378:8000:63bf:3fff:fdd2"
351        ))); // Teredo
352    }
353
354    // ---- SsrfPolicy ----
355
356    #[test]
357    fn ssrf_policy_default_is_public_https_only() {
358        assert_eq!(SsrfPolicy::default(), SsrfPolicy::PublicHttpsOnly);
359    }
360
361    #[test]
362    fn ssrf_policy_allows_internal() {
363        assert!(!SsrfPolicy::PublicHttpsOnly.allows_internal());
364        assert!(SsrfPolicy::AllowInternal.allows_internal());
365    }
366}