Skip to main content

servo_fetch/
net.rs

1//! Network address validation — blocks private, reserved, and special-purpose IPs.
2
3use std::sync::Once;
4
5use crate::error::{UrlError, map_url_error};
6
7/// Network access policy — determines which hosts are reachable.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct NetworkPolicy {
10    deny_private: bool,
11}
12
13impl NetworkPolicy {
14    /// Block all private/reserved addresses (production default).
15    pub const STRICT: Self = Self { deny_private: true };
16    /// Allow all addresses including private (testing only).
17    pub const PERMISSIVE: Self = Self { deny_private: false };
18
19    /// Check whether a host is allowed by this policy.
20    #[must_use]
21    pub fn is_host_allowed(self, host: &str) -> bool {
22        !self.deny_private || !is_private_host(host)
23    }
24}
25
26/// Validate a URL for fetching. Rejects disallowed schemes and private addresses
27/// based on the policy set via [`crate::init`].
28pub fn validate_url(url: &str) -> crate::error::Result<url::Url> {
29    validate_url_with_policy(url, crate::bridge::engine_policy()).map_err(|e| map_url_error(url, e))
30}
31
32/// Validate a URL against the given [`NetworkPolicy`].
33pub(crate) fn validate_url_with_policy(input: &str, policy: NetworkPolicy) -> Result<url::Url, UrlError> {
34    let mut parsed = url::Url::parse(input).map_err(|e| UrlError::Invalid(format!("invalid URL: {input}: {e}")))?;
35    match parsed.scheme() {
36        "http" | "https" => {}
37        s => {
38            return Err(UrlError::Invalid(format!(
39                "scheme '{s}' not allowed; only http:// and https:// are supported"
40            )));
41        }
42    }
43    if !parsed.username().is_empty() || parsed.password().is_some() {
44        tracing::warn!("credentials stripped from URL");
45        let _ = parsed.set_username("");
46        let _ = parsed.set_password(None);
47    }
48    if let Some(host) = parsed.host_str() {
49        if !policy.is_host_allowed(host) {
50            return Err(UrlError::PrivateAddress(host.to_string()));
51        }
52    }
53    Ok(parsed)
54}
55
56/// Replace CR, LF, and NUL with SP per RFC 9110.
57pub(crate) fn sanitize_user_agent(ua: String) -> String {
58    if ua.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
59        ua.replace(['\r', '\n', '\0'], " ")
60    } else {
61        ua
62    }
63}
64
65pub(crate) fn ensure_crypto_provider() {
66    static ONCE: Once = Once::new();
67    ONCE.call_once(|| {
68        if rustls::crypto::CryptoProvider::get_default().is_none() {
69            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
70        }
71    });
72}
73
74fn is_private_host(host: &str) -> bool {
75    const BLOCKED_HOSTS: &[&str] = &[
76        "localhost",
77        "127.0.0.1",
78        "[::1]",
79        "0.0.0.0",
80        "169.254.169.254",
81        "metadata.google.internal",
82    ];
83    let host = host.strip_suffix('.').unwrap_or(host);
84    if BLOCKED_HOSTS.iter().any(|&b| host.eq_ignore_ascii_case(b)) {
85        return true;
86    }
87    if let Ok(ip) = host.parse::<std::net::Ipv4Addr>() {
88        return is_private_ipv4(ip);
89    }
90    if let Ok(ip) = host
91        .trim_matches(|c| c == '[' || c == ']')
92        .parse::<std::net::Ipv6Addr>()
93    {
94        return is_private_ipv6(&ip);
95    }
96    false
97}
98
99/// IANA IPv4 Special-Purpose Address Registry (RFC 6890) + cloud metadata.
100fn is_private_ipv4(ip: std::net::Ipv4Addr) -> bool {
101    const BLOCKED: &[(u32, u32)] = &[
102        (0x0000_0000, 0xff00_0000), // 0.0.0.0/8        — RFC 1122 current network
103        (0x0a00_0000, 0xff00_0000), // 10.0.0.0/8       — RFC 1918 private
104        (0x6440_0000, 0xffc0_0000), // 100.64.0.0/10    — RFC 6598 shared/CGN
105        (0x7f00_0000, 0xff00_0000), // 127.0.0.0/8      — RFC 1122 loopback
106        (0xa9fe_0000, 0xffff_0000), // 169.254.0.0/16   — RFC 3927 link-local
107        (0xac10_0000, 0xfff0_0000), // 172.16.0.0/12    — RFC 1918 private
108        (0xc000_0000, 0xffff_ff00), // 192.0.0.0/24     — RFC 6890 IETF protocol
109        (0xc000_0200, 0xffff_ff00), // 192.0.2.0/24     — RFC 5737 TEST-NET-1
110        (0xc058_6300, 0xffff_ff00), // 192.88.99.0/24   — RFC 3068 6to4 relay
111        (0xc0a8_0000, 0xffff_0000), // 192.168.0.0/16   — RFC 1918 private
112        (0xc612_0000, 0xfffe_0000), // 198.18.0.0/15    — RFC 2544 benchmarking
113        (0xc633_6400, 0xffff_ff00), // 198.51.100.0/24  — RFC 5737 TEST-NET-2
114        (0xcb00_7100, 0xffff_ff00), // 203.0.113.0/24   — RFC 5737 TEST-NET-3
115        (0xe000_0000, 0xf000_0000), // 224.0.0.0/4      — RFC 5771 multicast
116        (0xf000_0000, 0xf000_0000), // 240.0.0.0/4      — RFC 1112 reserved
117        (0xffff_ffff, 0xffff_ffff), // 255.255.255.255  — broadcast
118    ];
119    let bits = u32::from(ip);
120    BLOCKED.iter().any(|&(net, mask)| bits & mask == net)
121}
122
123/// IANA IPv6 Special-Purpose Address Registry + IPv4-mapped/compatible.
124fn is_private_ipv6(ip: &std::net::Ipv6Addr) -> bool {
125    if let Some(v4) = ip.to_ipv4_mapped().or_else(|| ip.to_ipv4()) {
126        return is_private_ipv4(v4);
127    }
128    let seg = ip.segments();
129    let s0 = seg[0];
130    ip.is_loopback()
131        || ip.is_unspecified()
132        || (s0 == 0x0100 && seg[1] == 0 && seg[2] == 0 && seg[3] == 0) // 0100::/64 discard (RFC 6666)
133        || (s0 == 0x2001 && seg[1] == 0)                               // 2001::/32 Teredo
134        || (s0 == 0x2001 && seg[1] & 0xfff0 == 0x0010)                 // 2001:10::/28 ORCHID
135        || (s0 == 0x2001 && seg[1] & 0xfff0 == 0x0020)                 // 2001:20::/28 ORCHIDv2
136        || (s0 == 0x2001 && seg[1] == 0x0db8)                          // 2001:db8::/32 documentation
137        || s0 == 0x2002                                                // 2002::/16 6to4
138        || s0 & 0xfe00 == 0xfc00                                       // fc00::/7  unique local
139        || s0 & 0xffc0 == 0xfe80                                       // fe80::/10 link-local
140        || s0 & 0xff00 == 0xff00 // ff00::/8  multicast
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn blocks_localhost() {
149        assert!(is_private_host("localhost"));
150        assert!(is_private_host("127.0.0.1"));
151        assert!(is_private_host("[::1]"));
152    }
153
154    #[test]
155    fn blocks_private_ipv4() {
156        assert!(is_private_host("10.0.0.1"));
157        assert!(is_private_host("192.168.1.1"));
158        assert!(is_private_host("172.16.0.1"));
159    }
160
161    #[test]
162    fn blocks_shared_cgn() {
163        assert!(is_private_host("100.64.0.1"));
164        assert!(is_private_host("100.127.255.254"));
165    }
166
167    #[test]
168    fn blocks_documentation_ips() {
169        assert!(is_private_host("192.0.2.1"));
170        assert!(is_private_host("198.51.100.1"));
171        assert!(is_private_host("203.0.113.1"));
172    }
173
174    #[test]
175    fn blocks_multicast() {
176        assert!(is_private_host("224.0.0.1"));
177    }
178
179    #[test]
180    fn blocks_metadata() {
181        assert!(is_private_host("169.254.169.254"));
182        assert!(is_private_host("metadata.google.internal"));
183    }
184
185    #[test]
186    fn blocks_ipv4_mapped_ipv6() {
187        assert!(is_private_host("::ffff:127.0.0.1"));
188        assert!(is_private_host("::ffff:10.0.0.1"));
189    }
190
191    #[test]
192    fn blocks_ipv6_special() {
193        assert!(is_private_host("fe80::1"));
194        assert!(is_private_host("fd00::1"));
195        assert!(is_private_host("2001:db8::1"));
196    }
197
198    #[test]
199    fn allows_public() {
200        assert!(!is_private_host("8.8.8.8"));
201        assert!(!is_private_host("1.1.1.1"));
202        assert!(!is_private_host("example.com"));
203    }
204
205    #[test]
206    fn blocks_zero_address() {
207        assert!(is_private_host("0.0.0.0"));
208    }
209
210    #[test]
211    fn blocks_localhost_case_insensitive() {
212        assert!(is_private_host("LOCALHOST"));
213        assert!(is_private_host("Localhost"));
214    }
215
216    #[test]
217    fn blocks_broadcast() {
218        assert!(is_private_host("255.255.255.255"));
219    }
220
221    #[test]
222    fn blocks_reserved_240() {
223        assert!(is_private_host("240.0.0.1"));
224    }
225
226    #[test]
227    fn blocks_teredo() {
228        assert!(is_private_host("2001::1"));
229    }
230
231    #[test]
232    fn blocks_6to4() {
233        assert!(is_private_host("2002::1"));
234    }
235
236    #[test]
237    fn validate_url_empty_string() {
238        assert!(validate_url_with_policy("", NetworkPolicy::STRICT).is_err());
239    }
240
241    #[test]
242    fn blocks_trailing_dot() {
243        assert!(is_private_host("localhost."));
244        assert!(is_private_host("metadata.google.internal."));
245    }
246}