1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
//! Network egress checks for user-supplied URLs.
//!
//! The bot can be asked to fetch arbitrary URLs (`/download`, `/music`, cron
//! audio). Without a check, that is a server-side request forgery primitive:
//! loopback services, Docker host gateways, private LANs, and cloud metadata
//! (`169.254.169.254`) are all reachable from the container.
//!
//! This is a best-effort DNS check. It raises the bar substantially, but a
//! determined attacker can still use DNS rebinding between this check and the
//! actual request; combine it with network egress rules for full protection.
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use crate::error::{Error, Result};
/// Validate that `url` is an `http(s)` URL whose host resolves only to public
/// addresses. Call this before handing a user-supplied URL to reqwest, ffmpeg,
/// or yt-dlp.
pub async fn ensure_public_http_url(url: &str) -> Result<()> {
let parsed = reqwest::Url::parse(url)
.map_err(|e| Error::Custom(format!("Invalid URL: {e}")))?;
if !matches!(parsed.scheme(), "http" | "https") {
return Err(Error::Custom("Only http:// and https:// URLs are allowed".into()));
}
let host = parsed
.host_str()
.ok_or_else(|| Error::Custom("URL has no host".into()))?;
// `host_str` keeps IPv6 brackets.
let host = host.trim_start_matches('[').trim_end_matches(']');
let port = parsed.port_or_known_default().unwrap_or(80);
let mut resolved = false;
let addresses = tokio::net::lookup_host((host, port))
.await
.map_err(|e| Error::Custom(format!("Could not resolve {host}: {e}")))?;
for address in addresses {
resolved = true;
if !is_public_ip(address.ip()) {
return Err(Error::Custom(format!(
"Refusing to fetch a private or local address ({})",
address.ip()
)));
}
}
if !resolved {
return Err(Error::Custom(format!("Could not resolve {host}")));
}
Ok(())
}
fn is_public_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_public_v4(v4),
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
return is_public_v4(v4);
}
!(v6.is_loopback()
|| v6.is_unspecified()
|| v6.is_multicast()
// fc00::/7 unique-local
|| (v6.segments()[0] & 0xfe00) == 0xfc00
// fe80::/10 link-local
|| (v6.segments()[0] & 0xffc0) == 0xfe80
// 2001:db8::/32 documentation
|| (v6.segments()[0] == 0x2001 && v6.segments()[1] == 0x0db8))
}
}
}
fn is_public_v4(ip: Ipv4Addr) -> bool {
let octets = ip.octets();
!(ip.is_private()
|| ip.is_loopback()
|| ip.is_link_local()
|| ip.is_broadcast()
|| ip.is_documentation()
|| ip.is_unspecified()
|| ip.is_multicast()
// 0.0.0.0/8, 240.0.0.0/4 and other non-routable special ranges.
|| octets[0] == 0
|| octets[0] >= 240
// 100.64.0.0/10 carrier-grade NAT
|| (octets[0] == 100 && (64..=127).contains(&octets[1]))
// 192.0.0.0/24 IETF protocol assignments
|| (octets[0] == 192 && octets[1] == 0 && octets[2] == 0)
// 198.18.0.0/15 benchmarking
|| (octets[0] == 198 && (octets[1] == 18 || octets[1] == 19)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_and_special_ranges_are_blocked() {
for ip in [
"127.0.0.1",
"10.0.0.1",
"172.16.5.4",
"192.168.1.1",
"169.254.169.254",
"0.0.0.0",
"100.64.0.1",
"198.18.0.1",
"224.0.0.1",
"255.255.255.255",
"::1",
"fe80::1",
"fc00::1",
"::ffff:127.0.0.1",
] {
assert!(
!is_public_ip(ip.parse().unwrap()),
"{ip} should not be considered public"
);
}
}
#[test]
fn public_ranges_are_allowed() {
for ip in ["1.1.1.1", "8.8.8.8", "2606:4700:4700::1111"] {
assert!(
is_public_ip(ip.parse().unwrap()),
"{ip} should be public"
);
}
}
}
|