From d74822825c1ba98e9f96ca84a98b5c9e852357c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 21:22:11 +0300 Subject: [PATCH] security(core): reject the RFC 8215 local-use NAT64 prefix in the SSRF filter isBlockedIP admits only ipaddr.js 'unicast' addresses and relies on ipaddr.js to classify NAT64 as non-public. ipaddr.js@1.9.1 reports the well-known NAT64 prefix (64:ff9b::/96) as 'rfc6052' but the RFC 8215 local-use prefix (64:ff9b:1::/48) as generic unicast, so add an explicit check to reject that prefix, matching the existing NAT64 handling. Network-specific NAT64 prefixes carved from an operator's own unicast space cannot be distinguished by prefix and remain out of scope. Mirrors the same change in the MCP server's address classifier. Adds unit tests for 64:ff9b:1::7f00:1. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + api/utils/ssrf-protection.js | 10 ++++++++++ test/unit-tests/api.utils.ssrf-protection.js | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ace8a79ae..54748b67a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Enterprise Fixes: - [data-manager] Fixed editing an event whose key contains `&` creating undeletable duplicate rows in the events table Security Fixes: +- [core] The SSRF address filter now also rejects the RFC 8215 local-use NAT64 prefix (64:ff9b:1::/48), matching how it already handles the well-known NAT64 prefix - [hooks] Internal event hooks are now scoped to the apps the hook belongs to: app creation is a global-admin-only event, and remote-config, cohort, alert and hook-chaining events are only delivered when the event's app is one the hook is scoped to - [compliance-hub] The consents table now returns a fixed set of fields; a projection supplied on the request is no longer used to widen the response beyond the consent columns - [dashboards] Widgets are no longer copied when the copying user has no access to the apps they reference, and widget app ids are validated on widget create and update diff --git a/api/utils/ssrf-protection.js b/api/utils/ssrf-protection.js index 984c4554cd4..9d7d7ab5363 100644 --- a/api/utils/ssrf-protection.js +++ b/api/utils/ssrf-protection.js @@ -83,6 +83,16 @@ function isBlockedIP(ip) { return parsed.toIPv4Address().range() !== 'unicast'; } + // ipaddr.js reports the well-known NAT64 prefix (64:ff9b::/96) as 'rfc6052' + // (blocked by the unicast check below), but the RFC 8215 local-use NAT64 prefix + // (64:ff9b:1::/48) as generic unicast. Block it explicitly so a NAT64 gateway + // cannot translate its embedded IPv4 into an internal address. Network-specific + // NAT64 prefixes carved from an operator's own unicast space cannot be told + // apart by prefix and remain out of scope. + if (parsed.kind() === 'ipv6' && parsed.match(ipaddr.parseCIDR('64:ff9b:1::/48'))) { + return true; + } + return range !== 'unicast'; } diff --git a/test/unit-tests/api.utils.ssrf-protection.js b/test/unit-tests/api.utils.ssrf-protection.js index b0d4691565e..a10ccea533c 100644 --- a/test/unit-tests/api.utils.ssrf-protection.js +++ b/test/unit-tests/api.utils.ssrf-protection.js @@ -24,6 +24,13 @@ describe("SSRF protection utility", function() { it("allows a public IP literal", async function() { (await ssrf.isUrlSafe("http://8.8.8.8/")).safe.should.equal(true); }); + + it("blocks the RFC 8215 local-use NAT64 prefix (64:ff9b:1::/48)", async function() { + (await ssrf.isUrlSafe("http://[64:ff9b:1::7f00:1]/")).safe.should.equal(false); + }); + it("allows a public IPv6 literal", async function() { + (await ssrf.isUrlSafe("http://[2001:4860:4860::8888]/")).safe.should.equal(true); + }); }); describe("safeLookup (connect-time DNS pinning)", function() {