|
|
|
@ -240,7 +240,7 @@ fn validate_scheme(url: &url::Url) -> Result<(), AppError> {
|
|
|
|
/// Perform SSRF checks by resolving the URL's hostname and verifying
|
|
|
|
/// Perform SSRF checks by resolving the URL's hostname and verifying
|
|
|
|
/// that none of the resolved IP addresses are private, loopback,
|
|
|
|
/// that none of the resolved IP addresses are private, loopback,
|
|
|
|
/// or link-local.
|
|
|
|
/// or link-local.
|
|
|
|
async fn check_ssrf(url: &url::Url) -> Result<(), AppError> {
|
|
|
|
pub async fn check_ssrf(url: &url::Url) -> Result<(), AppError> {
|
|
|
|
let host = url
|
|
|
|
let host = url
|
|
|
|
.host_str()
|
|
|
|
.host_str()
|
|
|
|
.ok_or_else(|| AppError::BadRequest("URL has no host".into()))?;
|
|
|
|
.ok_or_else(|| AppError::BadRequest("URL has no host".into()))?;
|
|
|
|
@ -300,6 +300,13 @@ fn is_private_ip(ip: IpAddr) -> bool {
|
|
|
|
|| v4.is_unspecified() // 0.0.0.0
|
|
|
|
|| v4.is_unspecified() // 0.0.0.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
IpAddr::V6(v6) => {
|
|
|
|
IpAddr::V6(v6) => {
|
|
|
|
|
|
|
|
// Check for IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
|
|
|
|
|
|
|
|
if let Some(mapped_v4) = v6.to_ipv4_mapped() {
|
|
|
|
|
|
|
|
return mapped_v4.is_loopback()
|
|
|
|
|
|
|
|
|| mapped_v4.is_private()
|
|
|
|
|
|
|
|
|| mapped_v4.is_link_local()
|
|
|
|
|
|
|
|
|| mapped_v4.is_unspecified();
|
|
|
|
|
|
|
|
}
|
|
|
|
let segments = v6.segments();
|
|
|
|
let segments = v6.segments();
|
|
|
|
v6.is_loopback() // ::1
|
|
|
|
v6.is_loopback() // ::1
|
|
|
|
|| v6.is_unspecified() // ::
|
|
|
|
|| v6.is_unspecified() // ::
|
|
|
|
@ -781,6 +788,36 @@ mod tests {
|
|
|
|
assert!(!is_private_ip(ip));
|
|
|
|
assert!(!is_private_ip(ip));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn rejects_ipv4_mapped_ipv6_loopback() {
|
|
|
|
|
|
|
|
let ip: IpAddr = "::ffff:127.0.0.1".parse().unwrap();
|
|
|
|
|
|
|
|
assert!(is_private_ip(ip));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn rejects_ipv4_mapped_ipv6_private_10() {
|
|
|
|
|
|
|
|
let ip: IpAddr = "::ffff:10.0.0.1".parse().unwrap();
|
|
|
|
|
|
|
|
assert!(is_private_ip(ip));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn rejects_ipv4_mapped_ipv6_private_192() {
|
|
|
|
|
|
|
|
let ip: IpAddr = "::ffff:192.168.1.1".parse().unwrap();
|
|
|
|
|
|
|
|
assert!(is_private_ip(ip));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn rejects_ipv4_mapped_ipv6_link_local() {
|
|
|
|
|
|
|
|
let ip: IpAddr = "::ffff:169.254.1.1".parse().unwrap();
|
|
|
|
|
|
|
|
assert!(is_private_ip(ip));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
|
|
fn allows_ipv4_mapped_ipv6_public() {
|
|
|
|
|
|
|
|
let ip: IpAddr = "::ffff:8.8.8.8".parse().unwrap();
|
|
|
|
|
|
|
|
assert!(!is_private_ip(ip));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Soft-404 Detection ──────────────────────────────────────────
|
|
|
|
// ── Soft-404 Detection ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
#[test]
|
|
|
|
|