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/// Returns `true` if `ip` belongs to a network that must NOT be reached
20/// by an outbound HTTP client in this workspace.
21///
22/// Blocks:
23///
24/// - **IPv4**: private (RFC 1918), loopback (127.0.0.0/8), link-local
25///   (169.254.0.0/16 — cloud metadata), broadcast (255.255.255.255),
26///   multicast (224.0.0.0/4), unspecified (0.0.0.0), the entire
27///   `0.0.0.0/8` block (first octet 0), carrier-grade NAT
28///   (100.64.0.0/10 — RFC 6598), network interconnect benchmark
29///   (198.18.0.0/15 — RFC 2544), and the reserved future-use
30///   `240.0.0.0/4` block (RFC 1112).
31/// - **IPv6**: loopback (::1), multicast (ff00::/8), unspecified (::),
32///   unique-local (fc00::/7), link-local (fe80::/10), and deprecated
33///   site-local (fec0::/10 — RFC 3879).
34///   IPv4-mapped IPv6 (`::ffff:a.b.c.d`) inherits the classification of
35///   the embedded IPv4 — required to close the DNS-rebinding bypass where
36///   a public AAAA record maps to a private IPv4 in v4-mapped form.
37///
38/// Public, routable addresses (e.g. 8.8.8.8, 2001:4860:4860::8888) return `false`.
39pub fn is_ssrf_blocked_ip(ip: &IpAddr) -> bool {
40    match ip {
41        IpAddr::V4(v4) => {
42            v4.is_private()
43                || v4.is_loopback()
44                || v4.is_link_local()
45                || v4.is_broadcast()
46                || v4.is_multicast()
47                || v4.is_unspecified()
48                || v4.octets()[0] == 0
49                // CGN 100.64.0.0/10 (RFC 6598) — carrier-grade NAT range,
50                // commonly used as shared address space inside ISPs and
51                // occasionally leaked to internal networks.
52                || (v4.octets()[0] == 100
53                    && (v4.octets()[1] >= 64 && v4.octets()[1] <= 127))
54                // Network interconnect benchmark 198.18.0.0/15 (RFC 2544) —
55                // reserved for benchmarking; never appears on the public
56                // internet, only on lab equipment that an attacker could
57                // pivot through.
58                || (v4.octets()[0] == 198
59                    && (v4.octets()[1] == 18 || v4.octets()[1] == 19))
60                // Reserved future-use 240.0.0.0/4 (RFC 1112) — covers
61                // 240.0.0.0..255.255.255.254. Currently unallocated;
62                // blocking prevents any surprise assignment from becoming
63                // an SSRF target.
64                || v4.octets()[0] >= 240
65        }
66        IpAddr::V6(v6) => {
67            v6.is_loopback()
68                || v6.is_multicast()
69                || v6.is_unspecified()
70                // ULA fc00::/7 — covers both fc00::/8 and fd00::/8
71                || (v6.segments()[0] & 0xfe00) == 0xfc00
72                // Link-local fe80::/10
73                || (v6.segments()[0] & 0xffc0) == 0xfe80
74                // Deprecated site-local fec0::/10 (RFC 3879). Replaced by
75                // ULA but still routable in some legacy networks.
76                || (v6.segments()[0] & 0xffc0) == 0xfec0
77                // IPv4-mapped IPv6: recurse into the embedded IPv4 to
78                // close the rebinding bypass.
79                || v6
80                    .to_ipv4_mapped()
81                    .map(|v4| {
82                        v4.is_private()
83                            || v4.is_loopback()
84                            || v4.is_link_local()
85                            || v4.is_broadcast()
86                            || v4.is_multicast()
87                            || v4.is_unspecified()
88                            || v4.octets()[0] == 0
89                            || (v4.octets()[0] == 100
90                                && (v4.octets()[1] >= 64 && v4.octets()[1] <= 127))
91                            || (v4.octets()[0] == 198
92                                && (v4.octets()[1] == 18 || v4.octets()[1] == 19))
93                            || v4.octets()[0] >= 240
94                    })
95                    .unwrap_or(false)
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use std::net::Ipv4Addr;
104
105    fn v4(s: &str) -> IpAddr {
106        IpAddr::V4(s.parse::<Ipv4Addr>().expect("valid ipv4")) // allow-unwrap
107    }
108
109    fn v6(s: &str) -> IpAddr {
110        IpAddr::V6(s.parse().expect("valid ipv6")) // allow-unwrap
111    }
112
113    // ---- IPv4: blocked ranges ----
114
115    #[test]
116    fn blocks_rfc1918_10() {
117        assert!(is_ssrf_blocked_ip(&v4("10.0.0.1")));
118        assert!(is_ssrf_blocked_ip(&v4("10.255.255.255")));
119    }
120
121    #[test]
122    fn blocks_rfc1918_172_16() {
123        assert!(is_ssrf_blocked_ip(&v4("172.16.1.10")));
124        assert!(is_ssrf_blocked_ip(&v4("172.31.255.254")));
125    }
126
127    #[test]
128    fn blocks_rfc1918_192_168() {
129        assert!(is_ssrf_blocked_ip(&v4("192.168.1.1")));
130        assert!(is_ssrf_blocked_ip(&v4("192.168.0.0")));
131    }
132
133    #[test]
134    fn blocks_loopback_v4() {
135        assert!(is_ssrf_blocked_ip(&v4("127.0.0.1")));
136        assert!(is_ssrf_blocked_ip(&v4("127.255.255.254")));
137    }
138
139    #[test]
140    fn blocks_link_local_v4() {
141        // 169.254/16 — cloud metadata endpoints
142        assert!(is_ssrf_blocked_ip(&v4("169.254.169.254")));
143        assert!(is_ssrf_blocked_ip(&v4("169.254.1.1")));
144    }
145
146    #[test]
147    fn blocks_broadcast_v4() {
148        assert!(is_ssrf_blocked_ip(&v4("255.255.255.255")));
149    }
150
151    #[test]
152    fn blocks_multicast_v4() {
153        assert!(is_ssrf_blocked_ip(&v4("224.0.0.1")));
154        assert!(is_ssrf_blocked_ip(&v4("239.255.255.255")));
155    }
156
157    #[test]
158    fn blocks_unspecified_v4() {
159        assert!(is_ssrf_blocked_ip(&v4("0.0.0.0")));
160    }
161
162    #[test]
163    fn blocks_zero_octet_v4() {
164        // 0.0.0.0/8 — first octet 0, but not the unspecified address
165        assert!(is_ssrf_blocked_ip(&v4("0.1.2.3")));
166        assert!(is_ssrf_blocked_ip(&v4("0.255.255.255")));
167    }
168
169    #[test]
170    fn blocks_cgn_v4() {
171        // CGN 100.64.0.0/10 (RFC 6598) — first octet 100, second 64..=127
172        assert!(is_ssrf_blocked_ip(&v4("100.64.0.0")));
173        assert!(is_ssrf_blocked_ip(&v4("100.100.100.100")));
174        assert!(is_ssrf_blocked_ip(&v4("100.127.255.255")));
175        // 100.63 and 100.128 are NOT CGN
176        assert!(!is_ssrf_blocked_ip(&v4("100.63.255.255")));
177        assert!(!is_ssrf_blocked_ip(&v4("100.128.0.0")));
178    }
179
180    #[test]
181    fn blocks_benchmark_v4() {
182        // Benchmark 198.18.0.0/15 (RFC 2544) — second octet 18 or 19
183        assert!(is_ssrf_blocked_ip(&v4("198.18.0.0")));
184        assert!(is_ssrf_blocked_ip(&v4("198.18.255.255")));
185        assert!(is_ssrf_blocked_ip(&v4("198.19.255.255")));
186        // 198.17 and 198.20 are NOT benchmark
187        assert!(!is_ssrf_blocked_ip(&v4("198.17.255.255")));
188        assert!(!is_ssrf_blocked_ip(&v4("198.20.0.0")));
189    }
190
191    #[test]
192    fn blocks_reserved_v4() {
193        // Reserved 240.0.0.0/4 — first octet >= 240
194        assert!(is_ssrf_blocked_ip(&v4("240.0.0.0")));
195        assert!(is_ssrf_blocked_ip(&v4("241.1.2.3")));
196        assert!(is_ssrf_blocked_ip(&v4("250.100.200.50")));
197        // broadcast 255.255.255.255 already covered by is_broadcast
198        assert!(is_ssrf_blocked_ip(&v4("255.255.255.255")));
199        // 239.x is the top of multicast (224.0.0.0/4), NOT reserved —
200        // multicast is still blocked, but via a different rule.
201        assert!(is_ssrf_blocked_ip(&v4("239.255.255.255")));
202        // 100.x is CGN, blocked, not reserved
203        assert!(is_ssrf_blocked_ip(&v4("100.100.100.100")));
204    }
205
206    // ---- IPv4: allowed ranges ----
207
208    #[test]
209    fn allows_public_dns_v4() {
210        assert!(!is_ssrf_blocked_ip(&v4("8.8.8.8")));
211        assert!(!is_ssrf_blocked_ip(&v4("1.1.1.1")));
212    }
213
214    #[test]
215    fn allows_public_edge_v4() {
216        // 172.15 and 172.32 are NOT RFC-1918 (only 172.16/12 is)
217        assert!(!is_ssrf_blocked_ip(&v4("172.15.255.255")));
218        assert!(!is_ssrf_blocked_ip(&v4("172.32.0.0")));
219    }
220
221    // ---- IPv6: blocked ranges ----
222
223    #[test]
224    fn blocks_loopback_v6() {
225        assert!(is_ssrf_blocked_ip(&v6("::1")));
226    }
227
228    #[test]
229    fn blocks_unspecified_v6() {
230        assert!(is_ssrf_blocked_ip(&v6("::")));
231    }
232
233    #[test]
234    fn blocks_multicast_v6() {
235        assert!(is_ssrf_blocked_ip(&v6("ff02::1")));
236        assert!(is_ssrf_blocked_ip(&v6("ff00::1")));
237    }
238
239    #[test]
240    fn blocks_ula_fc_v6() {
241        assert!(is_ssrf_blocked_ip(&v6("fc00::1")));
242        assert!(is_ssrf_blocked_ip(&v6("fc00:1234:abcd::1")));
243    }
244
245    #[test]
246    fn blocks_ula_fd_v6() {
247        assert!(is_ssrf_blocked_ip(&v6("fd00::1")));
248        assert!(is_ssrf_blocked_ip(&v6("fd12:3456:789a::1")));
249    }
250
251    #[test]
252    fn blocks_link_local_v6() {
253        assert!(is_ssrf_blocked_ip(&v6("fe80::1")));
254        // fe80::/10 covers fe80..febf
255        assert!(is_ssrf_blocked_ip(&v6("febf:ffff::1")));
256        // febf + 1 is site-local, which is also blocked (separate test below)
257    }
258
259    #[test]
260    fn blocks_site_local_v6() {
261        // fec0::/10 (RFC 3879) — deprecated site-local, but still
262        // routable in some legacy networks.
263        assert!(is_ssrf_blocked_ip(&v6("fec0::1")));
264        assert!(is_ssrf_blocked_ip(&v6("feff:ffff::1")));
265        // febf is link-local, blocked by the fe80::/10 rule
266        assert!(is_ssrf_blocked_ip(&v6("febf::1")));
267        // ff00::/8 is multicast, already blocked
268        assert!(is_ssrf_blocked_ip(&v6("ff00::1")));
269    }
270
271    // ---- IPv6: allowed ranges ----
272
273    #[test]
274    fn allows_public_dns_v6() {
275        assert!(!is_ssrf_blocked_ip(&v6("2001:4860:4860::8888")));
276    }
277
278    #[test]
279    fn allows_public_documentation_v6() {
280        assert!(!is_ssrf_blocked_ip(&v6("2001:db8::1")));
281    }
282}