From 4a241e04bc5f826c16fe0f284010e0679cdd0892 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Sat, 5 Sep 2026 12:37:48 -0700 Subject: [PATCH] block 6to4/Teredo tunnels in the SSRF guard The IPv6 SSRF guard already re-checked IPv4-mapped/-compatible addresses, but 6to4 (2002::/16) and Teredo (2001:0000::/32) also tunnel an embedded IPv4 and were passed through untouched, letting an attacker wrap 127.0.0.1 or a cloud metadata address in either encapsulation to bypass the block list. Decode both forms and re-check the embedded IPv4 through is_blocked_ip. Add a test covering 6to4/Teredo-wrapped loopback and metadata addresses, plus a 6to4-wrapped public IP that must still be allowed. --- src/online.rs | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/online.rs b/src/online.rs index 8651f91..746de25 100644 --- a/src/online.rs +++ b/src/online.rs @@ -56,13 +56,24 @@ fn is_blocked_ip(ip: &IpAddr) -> bool { || v4.octets()[0] >= 240 } IpAddr::V6(v6) => { + let seg = v6.segments(); + // 6to4 (2002::/16) embeds an IPv4 in segments[1..3]; Teredo + // (2001:0000::/32) embeds the client IPv4 in the last two segments, + // each XOR 0xffff. Both must be re-checked as their embedded IPv4. + let sixtofour = (seg[0] == 0x2002) + .then(|| std::net::Ipv4Addr::from(((seg[1] as u32) << 16) | (seg[2] as u32))); + let teredo = (seg[0] == 0x2001 && seg[1] == 0x0000).then(|| { + std::net::Ipv4Addr::from( + (((seg[6] ^ 0xffff) as u32) << 16) | ((seg[7] ^ 0xffff) as u32), + ) + }); v6.is_loopback() || v6.is_unspecified() || v6.is_multicast() // Unique-local fc00::/7. - || (v6.segments()[0] & 0xfe00) == 0xfc00 + || (seg[0] & 0xfe00) == 0xfc00 // Link-local fe80::/10. - || (v6.segments()[0] & 0xffc0) == 0xfe80 + || (seg[0] & 0xffc0) == 0xfe80 // IPv4-mapped (::ffff:x.x.x.x) and IPv4-compatible (::x.x.x.x, // deprecated by RFC 4291 §2.5.5.1) — to_ipv4() returns Some for // both forms; re-check the embedded address as IPv4. @@ -70,6 +81,8 @@ fn is_blocked_ip(ip: &IpAddr) -> bool { .to_ipv4() .map(|m| is_blocked_ip(&IpAddr::V4(m))) == Some(true) + || sixtofour.is_some_and(|v4| is_blocked_ip(&IpAddr::V4(v4))) + || teredo.is_some_and(|v4| is_blocked_ip(&IpAddr::V4(v4))) } } } @@ -648,6 +661,30 @@ mod tests { )))); } + // 6to4 (2002::/16) and Teredo (2001:0000::/32) tunnel an IPv4 inside an + // IPv6 address; the guard must decode and re-check that embedded IPv4 or an + // internal target slips through the tunnel. + #[test] + fn ssrf_guard_blocks_embedded_ipv4_via_6to4_and_teredo() { + // 6to4 for 127.0.0.1: 2002:7f00:0001:: (embedded in segments[1..3]). + assert!(is_blocked_ip(&IpAddr::V6(Ipv6Addr::new( + 0x2002, 0x7f00, 0x0001, 0, 0, 0, 0, 0 + )))); + // 6to4 for 169.254.169.254 (cloud metadata): 2002:a9fe:a9fe::. + assert!(is_blocked_ip(&IpAddr::V6(Ipv6Addr::new( + 0x2002, 0xa9fe, 0xa9fe, 0, 0, 0, 0, 0 + )))); + // Teredo for 127.0.0.1: client IPv4 lives in the last two segments XOR + // 0xffff, so 0x7f00^0xffff=0x80ff and 0x0001^0xffff=0xfffe. + assert!(is_blocked_ip(&IpAddr::V6(Ipv6Addr::new( + 0x2001, 0x0000, 0, 0, 0, 0, 0x80ff, 0xfffe + )))); + // A 6to4 wrapping a PUBLIC IPv4 (8.8.8.8 → 2002:0808:0808::) is allowed. + assert!(!is_blocked_ip(&IpAddr::V6(Ipv6Addr::new( + 0x2002, 0x0808, 0x0808, 0, 0, 0, 0, 0 + )))); + } + #[test] fn ssrf_guard_allows_public_ips() { assert!(!is_blocked_ip(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))));