use std::net::IpAddr;
use phrona::extract_from_html;
use phrona::{HttpClient, extract, is_safe_ip};
fn expect_rejected(ip: &str) {
let ip: IpAddr = ip.parse().unwrap();
assert!(!is_safe_ip(ip), "{ip} must be rejected");
}
fn expect_allowed(ip: &str) {
let ip: IpAddr = ip.parse().unwrap();
assert!(is_safe_ip(ip), "{ip} must be allowed");
}
#[test]
fn rejects_all_private_ipv4_ranges() {
for ip in [
"0.0.0.0", "0.1.2.3", "10.0.0.1", "10.255.255.254", "100.64.0.1", "100.127.255.254", "127.0.0.1", "127.8.8.8", "169.254.169.254", "169.254.0.1", "172.16.0.1", "172.31.255.254", "192.0.0.1", "192.0.2.1", "192.88.99.1", "192.168.1.1", "198.18.0.1", "198.19.255.254", "198.51.100.1", "203.0.113.1", "224.0.0.1", "239.255.255.255", "240.0.0.1", "255.255.255.255", ] {
expect_rejected(ip);
}
}
#[test]
fn rejects_all_private_ipv6_ranges() {
for ip in [
"::1", "::", "::ffff:127.0.0.1", "::ffff:10.0.0.1", "::ffff:8.8.8.8", "::127.0.0.1", "64:ff9b:1::1", "100::1", "2001:db8::1", "2002::1", "fc00::1", "fd12:3456:789a::1", "fe80::1", "ff02::1", ] {
expect_rejected(ip);
}
}
#[test]
fn allows_public_addresses() {
for ip in [
"8.8.8.8",
"1.1.1.1",
"93.184.216.34",
"172.32.0.1", "169.255.0.1", "192.1.1.1", "2001:4860:4860::8888",
"2606:4700:4700::1111",
"2001:db9::1", ] {
expect_allowed(ip);
}
}
#[tokio::test]
async fn extract_blocks_dns_rebinding_vectors() {
let client = HttpClient::builder().build().unwrap();
for url in [
"http://localhost/",
"http://localhost:8080/",
"http://127.0.0.1/",
"http://127.0.0.1:8080/",
"http://169.254.169.254/latest/meta-data/",
"http://10.0.0.1/",
"http://192.168.1.1/",
"http://[::1]/",
"http://[::ffff:127.0.0.1]/",
"http://[fc00::1]/",
] {
let err = extract(&client, url, 100, None).await.unwrap_err();
assert!(err.to_string().contains("SSRF blocked"), "{url}: got {err}");
}
}
#[tokio::test]
async fn extract_blocks_non_http_schemes() {
let client = HttpClient::builder().build().unwrap();
for url in [
"javascript:alert(1)",
"javascript:document.location='http://evil.example/'",
"data:text/html,<script>alert(1)</script>",
"vbscript:msgbox(1)",
"file:///etc/passwd",
"ftp://example.com/file",
"",
"not a url",
] {
let err = extract(&client, url, 100, None).await.unwrap_err();
assert!(
err.to_string().contains("invalid query"),
"{url:?}: got {err}"
);
}
}
#[test]
fn extract_from_html_filters_xss_vectors() {
let html = r#"
<!doctype html><html><head><title><img src=x onerror=alert(1)></title>
<meta name="description" content="<script>alert(1)</script>">
</head><body><main>
<h1>Hello</h1>
<p>Trusted content.</p>
<img src="https://example.com/ok.png">
<img src="javascript:alert(1)">
<img src="data:text/html,<svg onload=alert(1)>">
<img src="vbscript:msgbox(1)">
<img src="//cdn.example.com/proto-relative.png">
<img src="/relative.png">
</main></body></html>"#;
let page = extract_from_html(html, "https://example.com/page", 500, None);
assert_eq!(page.images, ["https://example.com/ok.png"]);
assert!(!page.images.iter().any(|u| !u.starts_with("https://")));
assert_eq!(page.title, "<img src=x onerror=alert(1)>");
assert_eq!(page.description, "<script>alert(1)</script>");
assert!(page.text.contains("Trusted content."));
}