SSRF Guard Bypass via IPv6 Transition Addresses (NAT64/6to4) in TREK
Summary
The SSRF protection in TREK's ssrfGuard.ts does not block IPv6 transition addresses (NAT64 64:ff9b::/96, 6to4 2002::/16, Teredo 2001:0000::/32). An attacker who controls a DNS record can point it to a NAT64 or 6to4 address that embeds a private IPv4 target (e.g., 64:ff9b::169.254.169.254 for the AWS metadata service), bypassing the SSRF guard while the underlying network stack routes the connection to the embedded private IPv4 address.
Affected Component
- File:
server/src/utils/ssrfGuard.ts
- Functions:
isAlwaysBlocked(), isPrivateNetwork(), checkSsrf()
- Commit:
f9c992e
Root Cause
The isAlwaysBlocked function checks for loopback (127.*, ::1), unspecified (0.*), link-local (169.254.*, fe80:), and IPv4-mapped variants (::ffff:127.*, ::ffff:169.254.*). The isPrivateNetwork function checks RFC 1918 ranges, CGNAT, IPv6 ULA (fc00::/7), and IPv4-mapped RFC 1918 variants.
Neither function recognizes IPv6 transition addresses that embed an IPv4 address:
- NAT64 (
64:ff9b::/96): 64:ff9b::7f00:1 embeds 127.0.0.1
- 6to4 (
2002::/16): 2002:7f00:0001:: embeds 127.0.0.1
- Teredo (
2001:0000::/32): last 32 bits encode a client IPv4 (XOR'd with 0xFFFFFFFF)
When a hostname resolves to such an address, checkSsrf() classifies it as "allowed" because none of the regex patterns or prefix checks match these ranges.
Affected Code Paths
Multiple user-facing features use the vulnerable SSRF guard:
-
Google Maps list import (server/src/services/placeService.ts:723): importGoogleList(tripId, url) accepts a user-provided URL, calls checkSsrf(url), then fetches it with safeFetchFollow().
-
Naver Maps list import (server/src/services/placeService.ts:846): importNaverList(tripId, url) same pattern.
-
Webhook notifications (server/src/services/notifications.ts:320,457): webhook delivery URLs validated by checkSsrf().
-
Immich/Synology/AirTrail integrations: Server URLs validated by checkSsrf() or safeFetch().
-
Plugin egress policy (server/src/nest/plugins/runtime/egress-policy.ts): isBlockedIp() checks IPv4-mapped and IPv4-compatible forms but also misses NAT64/6to4/Teredo.
Proof of Concept
-
Set up a DNS record for attacker.example.com with an AAAA record pointing to 64:ff9b::a9fe:a9fe (NAT64 encoding of 169.254.169.254).
-
As a TREK user, import a Google Maps list using the URL http://attacker.example.com/.
-
checkSsrf() resolves attacker.example.com to 64:ff9b::a9fe:a9fe. Neither isAlwaysBlocked() nor isPrivateNetwork() recognizes this as a blocked address. The request proceeds.
-
On a deployment with NAT64 gateway support (common in IPv6-only cloud environments), the network stack translates the connection to 169.254.169.254, reaching the cloud metadata service.
Impact
- Cloud credential theft: In cloud environments with NAT64, the SSRF guard bypass allows reading cloud metadata endpoints, potentially exposing IAM credentials, instance identity tokens, and other sensitive data.
- Internal network scanning: Mapping internal services by observing connection success/failure.
- Data exfiltration: Reading responses from internal services (the response is processed by
safeFetchFollow and its callers).
Suggested Fix
Add checks for IPv6 transition address ranges in both isAlwaysBlocked and isPrivateNetwork. For each recognized transition prefix, extract the embedded IPv4 address and validate it against the existing IPv4 blocklists:
// NAT64 well-known prefix (64:ff9b::/96) - last 4 bytes are the IPv4 address
if (/^64:ff9b:/i.test(addr)) {
const embedded = extractEmbeddedIpv4FromNat64(addr);
if (embedded && (isAlwaysBlocked(embedded) || isPrivateNetwork(embedded))) return true;
}
// 6to4 (2002::/16) - bytes 2-5 are the IPv4 address
if (/^2002:/i.test(addr)) {
const embedded = extractEmbeddedIpv4From6to4(addr);
if (embedded && (isAlwaysBlocked(embedded) || isPrivateNetwork(embedded))) return true;
}
// Teredo (2001:0000::/32) - last 4 bytes XOR 0xFFFFFFFF
if (/^2001:0000:/i.test(addr) || /^2001:0:/i.test(addr)) {
const embedded = extractEmbeddedIpv4FromTeredo(addr);
if (embedded && (isAlwaysBlocked(embedded) || isPrivateNetwork(embedded))) return true;
}
Severity
Moderate (CWE-918: Server-Side Request Forgery)
Credit
tonghuaroot (tonghuaroot@gmail.com)
SSRF Guard Bypass via IPv6 Transition Addresses (NAT64/6to4) in TREK
Summary
The SSRF protection in TREK's
ssrfGuard.tsdoes not block IPv6 transition addresses (NAT6464:ff9b::/96, 6to42002::/16, Teredo2001:0000::/32). An attacker who controls a DNS record can point it to a NAT64 or 6to4 address that embeds a private IPv4 target (e.g.,64:ff9b::169.254.169.254for the AWS metadata service), bypassing the SSRF guard while the underlying network stack routes the connection to the embedded private IPv4 address.Affected Component
server/src/utils/ssrfGuard.tsisAlwaysBlocked(),isPrivateNetwork(),checkSsrf()f9c992eRoot Cause
The
isAlwaysBlockedfunction checks for loopback (127.*,::1), unspecified (0.*), link-local (169.254.*,fe80:), and IPv4-mapped variants (::ffff:127.*,::ffff:169.254.*). TheisPrivateNetworkfunction checks RFC 1918 ranges, CGNAT, IPv6 ULA (fc00::/7), and IPv4-mapped RFC 1918 variants.Neither function recognizes IPv6 transition addresses that embed an IPv4 address:
64:ff9b::/96):64:ff9b::7f00:1embeds127.0.0.12002::/16):2002:7f00:0001::embeds127.0.0.12001:0000::/32): last 32 bits encode a client IPv4 (XOR'd with0xFFFFFFFF)When a hostname resolves to such an address,
checkSsrf()classifies it as "allowed" because none of the regex patterns or prefix checks match these ranges.Affected Code Paths
Multiple user-facing features use the vulnerable SSRF guard:
Google Maps list import (
server/src/services/placeService.ts:723):importGoogleList(tripId, url)accepts a user-provided URL, callscheckSsrf(url), then fetches it withsafeFetchFollow().Naver Maps list import (
server/src/services/placeService.ts:846):importNaverList(tripId, url)same pattern.Webhook notifications (
server/src/services/notifications.ts:320,457): webhook delivery URLs validated bycheckSsrf().Immich/Synology/AirTrail integrations: Server URLs validated by
checkSsrf()orsafeFetch().Plugin egress policy (
server/src/nest/plugins/runtime/egress-policy.ts):isBlockedIp()checks IPv4-mapped and IPv4-compatible forms but also misses NAT64/6to4/Teredo.Proof of Concept
Set up a DNS record for
attacker.example.comwith an AAAA record pointing to64:ff9b::a9fe:a9fe(NAT64 encoding of169.254.169.254).As a TREK user, import a Google Maps list using the URL
http://attacker.example.com/.checkSsrf()resolvesattacker.example.comto64:ff9b::a9fe:a9fe. NeitherisAlwaysBlocked()norisPrivateNetwork()recognizes this as a blocked address. The request proceeds.On a deployment with NAT64 gateway support (common in IPv6-only cloud environments), the network stack translates the connection to
169.254.169.254, reaching the cloud metadata service.Impact
safeFetchFollowand its callers).Suggested Fix
Add checks for IPv6 transition address ranges in both
isAlwaysBlockedandisPrivateNetwork. For each recognized transition prefix, extract the embedded IPv4 address and validate it against the existing IPv4 blocklists:Severity
Moderate (CWE-918: Server-Side Request Forgery)
Credit
tonghuaroot (tonghuaroot@gmail.com)