mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
7.2 KiB
Rust
244 lines
7.2 KiB
Rust
//! SSRF protection unit tests
|
|
//!
|
|
//! Tests for the `synctv_common::ssrf` ACL with Teredo (`2001::/32`), 6to4 (`2002::/16`),
|
|
//! IPv4-mapped IPv6, and additional edge cases.
|
|
|
|
#![allow(clippy::unwrap_used)]
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
use synctv_common::ssrf::SsrfGuard;
|
|
|
|
fn is_ip_blocked_by_disabled_policy(ip: &IpAddr) -> bool {
|
|
SsrfGuard::disabled().is_ip_blocked(ip)
|
|
}
|
|
|
|
fn is_ip_blocked_by_strict_policy(ip: &IpAddr) -> bool {
|
|
SsrfGuard::strict_policy().is_ip_blocked(ip)
|
|
}
|
|
|
|
#[test]
|
|
fn test_disabled_policy_has_no_acl() {
|
|
assert!(SsrfGuard::disabled().acl().is_none());
|
|
assert!(SsrfGuard::disabled().dns_resolver().is_none());
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V4(
|
|
Ipv4Addr::LOCALHOST
|
|
)));
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V6(
|
|
Ipv6Addr::LOCALHOST
|
|
)));
|
|
}
|
|
|
|
// Teredo IPv6 (2001:0000::/32) blocking
|
|
|
|
#[test]
|
|
fn test_teredo_ipv6_blocked() {
|
|
let teredo = Ipv6Addr::new(
|
|
0x2001, 0x0000, 0x1234, 0x5678, 0x9abc, 0xdef0, 0x1111, 0x2222,
|
|
);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(teredo)),
|
|
"Teredo addresses (2001:0000::/32) must be blocked"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_teredo_ipv6_various_payloads() {
|
|
let addrs = [
|
|
Ipv6Addr::new(
|
|
0x2001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
|
),
|
|
Ipv6Addr::new(
|
|
0x2001, 0x0000, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
|
|
),
|
|
Ipv6Addr::new(
|
|
0x2001, 0x0000, 0x4136, 0xe378, 0x8000, 0x63bf, 0x3fff, 0xfdd2,
|
|
),
|
|
];
|
|
for addr in &addrs {
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(*addr)),
|
|
"Teredo address {addr} must be blocked"
|
|
);
|
|
}
|
|
}
|
|
|
|
// 6to4 IPv6 (2002::/16) blocking
|
|
|
|
#[test]
|
|
fn test_6to4_ipv6_blocked() {
|
|
let six_to_four = Ipv6Addr::new(
|
|
0x2002, 0xc0a8, 0x0101, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
|
);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(six_to_four)),
|
|
"6to4 addresses (2002::/16) must be blocked"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_6to4_ipv6_encapsulating_public() {
|
|
// 6to4 encapsulating 8.8.8.8 -> 2002:0808:0808::1
|
|
// Still blocked because 6to4 tunnel is inherently dangerous
|
|
let addr = Ipv6Addr::new(
|
|
0x2002, 0x0808, 0x0808, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
|
);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(addr)),
|
|
"6to4 even with public IPv4 payload must be blocked"
|
|
);
|
|
}
|
|
|
|
// IPv4-mapped IPv6 addresses
|
|
|
|
#[test]
|
|
fn test_ipv4_mapped_ipv6_private_blocked() {
|
|
// ::ffff:127.0.0.1
|
|
let mapped_loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x7f00, 0x0001);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(mapped_loopback)),
|
|
"IPv4-mapped loopback must be blocked"
|
|
);
|
|
|
|
// ::ffff:192.168.1.1
|
|
let mapped_private = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0101);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(mapped_private)),
|
|
"IPv4-mapped private must be blocked"
|
|
);
|
|
|
|
// ::ffff:10.0.0.1
|
|
let mapped_10 = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x0a00, 0x0001);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(mapped_10)),
|
|
"IPv4-mapped 10.x must be blocked"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ipv4_mapped_ipv6_public_blocked() {
|
|
// Note: IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) are blocked by default
|
|
// because the underlying http-acl library doesn't convert them to IPv4
|
|
// before checking. This is intentional security behavior - IPv4-mapped
|
|
// IPv6 addresses can be used to bypass IP-based restrictions.
|
|
// ::ffff:8.8.8.8
|
|
let mapped_public = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x0808, 0x0808);
|
|
assert!(
|
|
is_ip_blocked_by_strict_policy(&IpAddr::V6(mapped_public)),
|
|
"IPv4-mapped IPv6 addresses should be blocked for security reasons"
|
|
);
|
|
}
|
|
|
|
// IPv6 unique-local and link-local
|
|
|
|
#[test]
|
|
fn test_ipv6_unique_local_blocked() {
|
|
let fc00 = Ipv6Addr::new(0xfc00, 0, 0, 0, 0, 0, 0, 1);
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(fc00)));
|
|
|
|
let fd00 = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1);
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(fd00)));
|
|
|
|
let fdff = Ipv6Addr::new(
|
|
0xfdff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
|
|
);
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(fdff)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ipv6_link_local_blocked() {
|
|
let link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(link_local)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ipv6_multicast_blocked() {
|
|
let multicast = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1);
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(multicast)));
|
|
}
|
|
|
|
// IPv6 global unicast (allowed)
|
|
|
|
#[test]
|
|
fn test_ipv6_global_unicast_allowed() {
|
|
let cloudflare = Ipv6Addr::new(0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111);
|
|
assert!(
|
|
!is_ip_blocked_by_disabled_policy(&IpAddr::V6(cloudflare)),
|
|
"Global unicast IPv6 should be allowed by the disabled policy"
|
|
);
|
|
assert!(
|
|
!is_ip_blocked_by_strict_policy(&IpAddr::V6(cloudflare)),
|
|
"Global unicast IPv6 should be allowed"
|
|
);
|
|
|
|
let public = Ipv6Addr::new(0x2400, 0xcb00, 0, 0, 0, 0, 0, 1);
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V6(public)));
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V6(public)));
|
|
}
|
|
|
|
// is_ip_blocked dispatch
|
|
|
|
#[test]
|
|
fn test_is_ip_blocked_v4() {
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V4(
|
|
Ipv4Addr::LOCALHOST
|
|
)));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V4(
|
|
Ipv4Addr::LOCALHOST
|
|
)));
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V4(
|
|
Ipv4Addr::new(8, 8, 8, 8)
|
|
)));
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
8, 8, 8, 8
|
|
))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_ip_blocked_v6() {
|
|
assert!(!is_ip_blocked_by_disabled_policy(&IpAddr::V6(
|
|
Ipv6Addr::LOCALHOST
|
|
)));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(
|
|
Ipv6Addr::LOCALHOST
|
|
)));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V6(Ipv6Addr::new(
|
|
0x2001, 0, 0, 0, 0, 0, 0, 1
|
|
))));
|
|
}
|
|
|
|
// IPv4 boundary tests
|
|
|
|
#[test]
|
|
fn test_ipv4_172_range_boundary() {
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
172, 15, 255, 255
|
|
))));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
172, 16, 0, 0
|
|
))));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
172, 31, 255, 255
|
|
))));
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
172, 32, 0, 0
|
|
))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ipv4_cgnat_boundary() {
|
|
// Just below CGNAT range should be allowed
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
100, 63, 255, 255
|
|
))));
|
|
// CGNAT range should be blocked
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
100, 64, 0, 0
|
|
))));
|
|
assert!(is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
100, 127, 255, 255
|
|
))));
|
|
// Just above CGNAT range should be allowed
|
|
assert!(!is_ip_blocked_by_strict_policy(&IpAddr::V4(Ipv4Addr::new(
|
|
100, 128, 0, 0
|
|
))));
|
|
}
|