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