1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
14
15use url::{Host, Url};
16
17const ALLOW_PRIVATE_ENV: &str = "WEBFETCH_ALLOW_PRIVATE";
19
20pub fn allow_private() -> bool {
22 matches!(
23 std::env::var(ALLOW_PRIVATE_ENV).ok().as_deref(),
24 Some("1") | Some("true") | Some("TRUE")
25 )
26}
27
28pub fn is_blocked_ip(ip: IpAddr) -> bool {
33 match ip {
34 IpAddr::V4(v4) => is_blocked_ipv4(v4),
35 IpAddr::V6(v6) => is_blocked_ipv6(v6),
36 }
37}
38
39fn is_blocked_ipv4(ip: Ipv4Addr) -> bool {
40 let o = ip.octets();
41 ip.is_loopback() || ip.is_private() || ip.is_link_local() || ip.is_broadcast() || ip.is_unspecified() || ip.is_multicast() || ip.is_documentation() || o[0] == 0 || (o[0] == 100 && (o[1] & 0xc0) == 64) || (o[0] == 192 && o[1] == 0 && o[2] == 0) || (o[0] == 198 && (o[1] & 0xfe) == 18) || o[0] >= 240 }
54
55fn is_blocked_ipv6(ip: Ipv6Addr) -> bool {
56 if let Some(v4) = ip.to_ipv4_mapped() {
58 return is_blocked_ipv4(v4);
59 }
60 if let Some(v4) = ip.to_ipv4() {
61 return is_blocked_ipv4(v4);
63 }
64 let seg = ip.segments();
65 ip.is_loopback()
66 || ip.is_unspecified()
67 || ip.is_multicast()
68 || (seg[0] & 0xffc0) == 0xfe80 || (seg[0] & 0xfe00) == 0xfc00 || (seg[0] == 0x2001 && seg[1] == 0x0db8) }
72
73#[derive(Debug)]
75pub struct BlockedUrl(pub String);
76
77impl std::fmt::Display for BlockedUrl {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "blocked URL: {}", self.0)
80 }
81}
82
83impl std::error::Error for BlockedUrl {}
84
85pub fn validate_url(url: &Url) -> Result<Vec<std::net::SocketAddr>, BlockedUrl> {
91 if allow_private() {
92 return Ok(Vec::new());
93 }
94
95 match url.scheme() {
96 "http" | "https" => {}
97 other => return Err(BlockedUrl(format!("scheme `{other}` not allowed"))),
98 }
99
100 let host = url
101 .host()
102 .ok_or_else(|| BlockedUrl(format!("no host in {url}")))?;
103
104 match host {
105 Host::Ipv4(ip) => {
106 if is_blocked_ip(IpAddr::V4(ip)) {
107 return Err(BlockedUrl(format!("host IP {ip} is not public")));
108 }
109 Ok(Vec::new())
110 }
111 Host::Ipv6(ip) => {
112 if is_blocked_ip(IpAddr::V6(ip)) {
113 return Err(BlockedUrl(format!("host IP {ip} is not public")));
114 }
115 Ok(Vec::new())
116 }
117 Host::Domain(domain) => validate_domain(url, domain),
118 }
119}
120
121fn validate_domain(url: &Url, domain: &str) -> Result<Vec<std::net::SocketAddr>, BlockedUrl> {
122 let lower = domain.to_ascii_lowercase();
124 if lower == "localhost" || lower.ends_with(".localhost") {
125 return Err(BlockedUrl(format!("host `{domain}` is local")));
126 }
127
128 let port = url
129 .port_or_known_default()
130 .ok_or_else(|| BlockedUrl(format!("no port for {url}")))?;
131
132 let addrs: Vec<_> = (domain, port)
135 .to_socket_addrs()
136 .map_err(|e| BlockedUrl(format!("cannot resolve `{domain}`: {e}")))?
137 .collect();
138
139 if addrs.is_empty() {
140 return Err(BlockedUrl(format!("`{domain}` resolved to no addresses")));
141 }
142 for addr in &addrs {
143 if is_blocked_ip(addr.ip()) {
144 return Err(BlockedUrl(format!(
145 "`{domain}` resolves to non-public IP {}",
146 addr.ip()
147 )));
148 }
149 }
150 Ok(addrs)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn blocked(s: &str) -> bool {
158 is_blocked_ip(s.parse::<IpAddr>().unwrap())
159 }
160
161 #[test]
162 fn blocks_loopback_and_private_and_metadata() {
163 assert!(blocked("127.0.0.1"));
164 assert!(blocked("10.0.0.1"));
165 assert!(blocked("172.16.5.4"));
166 assert!(blocked("192.168.1.1"));
167 assert!(blocked("169.254.169.254")); assert!(blocked("100.64.0.1")); assert!(blocked("0.0.0.0"));
170 assert!(blocked("255.255.255.255"));
171 assert!(blocked("224.0.0.1")); assert!(blocked("240.0.0.1")); }
174
175 #[test]
176 fn blocks_ipv6_local_and_mapped() {
177 assert!(blocked("::1")); assert!(blocked("::")); assert!(blocked("fe80::1")); assert!(blocked("fc00::1")); assert!(blocked("::ffff:127.0.0.1")); assert!(blocked("::ffff:169.254.169.254")); }
184
185 #[test]
186 fn allows_public() {
187 assert!(!blocked("1.1.1.1"));
188 assert!(!blocked("8.8.8.8"));
189 assert!(!blocked("93.184.216.34")); assert!(!blocked("2606:4700:4700::1111")); }
192
193 #[test]
194 fn rejects_non_http_scheme() {
195 let url = Url::parse("file:///etc/passwd").unwrap();
196 assert!(validate_url(&url).is_err());
197 let url = Url::parse("ftp://example.com/x").unwrap();
198 assert!(validate_url(&url).is_err());
199 }
200
201 #[test]
202 fn rejects_literal_metadata_ip_url() {
203 let url = Url::parse("http://169.254.169.254/latest/meta-data/").unwrap();
204 assert!(validate_url(&url).is_err());
205 }
206
207 #[test]
208 fn rejects_localhost_name() {
209 let url = Url::parse("http://localhost:8080/admin").unwrap();
210 assert!(validate_url(&url).is_err());
211 }
212}