diff --git a/packages/loopover-engine/src/review/safe-url.ts b/packages/loopover-engine/src/review/safe-url.ts index 6395e78718..93b7622165 100644 --- a/packages/loopover-engine/src/review/safe-url.ts +++ b/packages/loopover-engine/src/review/safe-url.ts @@ -51,6 +51,7 @@ function ipv4IsPrivateOrLocal(host: string): boolean { if (a === 169 && b === 254) return true; // link-local (incl. cloud metadata 169.254.169.254) if (a === 192 && b === 168) return true; if (a === 172 && b >= 16 && b <= 31) return true; + if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 shared address space (RFC 6598 CGNAT) return false; } diff --git a/src/review/content-lane/safe-url.ts b/src/review/content-lane/safe-url.ts index 6395e78718..93b7622165 100644 --- a/src/review/content-lane/safe-url.ts +++ b/src/review/content-lane/safe-url.ts @@ -51,6 +51,7 @@ function ipv4IsPrivateOrLocal(host: string): boolean { if (a === 169 && b === 254) return true; // link-local (incl. cloud metadata 169.254.169.254) if (a === 192 && b === 168) return true; if (a === 172 && b >= 16 && b <= 31) return true; + if (a === 100 && b >= 64 && b <= 127) return true; // 100.64.0.0/10 shared address space (RFC 6598 CGNAT) return false; } diff --git a/test/unit/content-lane-safe-url.test.ts b/test/unit/content-lane-safe-url.test.ts index 0c180171aa..1d12d9752c 100644 --- a/test/unit/content-lane-safe-url.test.ts +++ b/test/unit/content-lane-safe-url.test.ts @@ -24,6 +24,17 @@ describe("isSafeHttpUrl", () => { expect(isSafeHttpUrl("https://printer.local")).toBe(false); }); + it("rejects the RFC 6598 shared-address-space (CGNAT) range 100.64.0.0/10 (#7253)", () => { + // A URL resolving to a carrier-grade-NAT / shared-address-space host is internal, not publicly routable, + // and must be blocked like the other private ranges. Boundary-tested at the /10's inclusive edges. + expect(isSafeHttpUrl("https://100.64.0.0")).toBe(false); // lower inclusive bound + expect(isSafeHttpUrl("https://100.100.50.1")).toBe(false); // mid-range + expect(isSafeHttpUrl("https://100.127.255.255")).toBe(false); // upper inclusive bound + // Just outside the /10 in either direction must remain public/safe. + expect(isSafeHttpUrl("https://100.63.255.255")).toBe(true); // one below the range + expect(isSafeHttpUrl("https://100.128.0.0")).toBe(true); // one above the range + }); + it("rejects the RFC 6761 *.localhost loopback namespace (not just bare localhost)", () => { // RFC 6761 makes every `*.localhost` name loopback (systemd-resolved, browsers), so the bare // `=== "localhost"` check leaked sub-labelled forms; `.endsWith(".localhost")` closes them. diff --git a/test/unit/safe-url-engine.test.ts b/test/unit/safe-url-engine.test.ts index e58ee37c33..e1d3b8ed7b 100644 --- a/test/unit/safe-url-engine.test.ts +++ b/test/unit/safe-url-engine.test.ts @@ -25,6 +25,17 @@ describe("isSafeHttpUrl", () => { expect(isSafeHttpUrl("https://printer.local")).toBe(false); }); + it("rejects the RFC 6598 shared-address-space (CGNAT) range 100.64.0.0/10 (#7253)", () => { + // A URL resolving to a carrier-grade-NAT / shared-address-space host is internal, not publicly routable, + // and must be blocked like the other private ranges. Boundary-tested at the /10's inclusive edges. + expect(isSafeHttpUrl("https://100.64.0.0")).toBe(false); // lower inclusive bound + expect(isSafeHttpUrl("https://100.100.50.1")).toBe(false); // mid-range + expect(isSafeHttpUrl("https://100.127.255.255")).toBe(false); // upper inclusive bound + // Just outside the /10 in either direction must remain public/safe. + expect(isSafeHttpUrl("https://100.63.255.255")).toBe(true); // one below the range + expect(isSafeHttpUrl("https://100.128.0.0")).toBe(true); // one above the range + }); + it("rejects the RFC 6761 *.localhost loopback namespace (not just bare localhost)", () => { // RFC 6761 makes every `*.localhost` name loopback (systemd-resolved, browsers), so the bare // `=== "localhost"` check leaked sub-labelled forms; `.endsWith(".localhost")` closes them.