Skip to content

Commit 8aa746d

Browse files
fix(origin-gate): stop over-blocking the two reachable /32s in 192.0.0.0/24
192.0.0.0/24 carries "Globally Reachable: False" as a block, but the IANA IPv4 Special-Purpose registry re-delegates two /32s inside it that are marked True: 192.0.0.9/32 Port Control Protocol Anycast (RFC 7723) 192.0.0.10/32 TURN Anycast (RFC 8155) Refusing the /24 wholesale over-blocks both. The uncomfortable part: this file's IPv6 side already carves out the very same two services (2001:1::1 PCP and 2001:1::2 TURN) inside 2001::/23, with a comment warning that the registry -- not the intuition that a prefix "looks internal" -- is the authority. The same reasoning was applied on one side and skipped on the other. Why it was missed: the bidirectional difference check that produced the IPv6 carve-outs was run over the IPv6 registry ONLY. The claim it was used to support ("registry-exact in both directions") was stated for the gate as a whole. A completeness claim was therefore made from a check covering half its subject -- the same class the surrounding commits were written to close. Found by an independent audit, not by me. Now verified over BOTH registries, both directions, all four difference sets empty: IPv4 A-B (registry-False, not refused) -> empty IPv4 B-A (registry-True, refused) -> empty IPv6 A-B -> empty IPv6 B-A -> empty Test pins the carve-out at exactly two addresses wide: 192.0.0.9 and .10 stay accepted, 192.0.0.8/.11/.1 stay refused. Discrimination proved -- removing the carve-out turns the suite red (194/1); restored, 195/195.
1 parent 4ec63df commit 8aa746d

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

src/security/header-values.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,20 @@ function parseCanonicalIpv4(hostname) {
388388
}
389389

390390
function isNonPublicIpv4(octets) {
391-
const [a, b, c] = octets;
391+
const [a, b, c, d] = octets;
392+
393+
// 192.0.0.0/24 is registry-False as a block, but the registry re-delegates two
394+
// /32s inside it that ARE globally reachable: 192.0.0.9 (Port Control Protocol
395+
// Anycast, RFC 7723) and 192.0.0.10 (TURN Anycast, RFC 8155). Refusing the /24
396+
// wholesale over-blocks them.
397+
//
398+
// The IPv6 side of this file already carves out the very same two services
399+
// (2001:1::1 and 2001:1::2) inside 2001::/23 — this is that carve-out's
400+
// missing IPv4 twin. Found by an independent audit, not by the bidirectional
401+
// registry check that produced the IPv6 carve-outs: that check was run over
402+
// the IPv6 registry only, so the IPv4 half of the same claim went untested.
403+
if (a === 192 && b === 0 && c === 0 && (d === 9 || d === 10)) return false;
404+
392405
return (
393406
a === 0 ||
394407
a === 10 ||

tests/security/production-boundary.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,3 +372,30 @@ test('Integrity-Policy-Report-Only cannot be smuggled onto the served response',
372372
'expected the report-only twin to be refused at the response boundary',
373373
);
374374
});
375+
376+
test('the two globally reachable /32s inside 192.0.0.0/24 stay accepted', () => {
377+
// 192.0.0.0/24 is registry-False as a block, but the registry re-delegates two
378+
// /32s inside it that carry "Globally Reachable: True". Refusing the /24
379+
// wholesale over-blocks them. This is the IPv4 twin of the 2001:1::1/2/3
380+
// carve-out below — it was missing because the bidirectional registry check
381+
// that produced the IPv6 carve-outs was only ever run over the IPv6 registry.
382+
for (const origin of [
383+
'https://192.0.0.9', // Port Control Protocol Anycast (RFC 7723)
384+
'https://192.0.0.10', // TURN Anycast (RFC 8155)
385+
]) {
386+
assert.deepEqual(
387+
validateApprovedEndpointOrigins([origin]),
388+
[],
389+
`expected ${origin} to remain accepted (registry: Globally Reachable = True)`,
390+
);
391+
}
392+
393+
// The surrounding /24 must still be refused — the carve-out is exactly two
394+
// addresses wide, not a hole in the block.
395+
for (const origin of ['https://192.0.0.8', 'https://192.0.0.11', 'https://192.0.0.1']) {
396+
assert.ok(
397+
validateApprovedEndpointOrigins([origin]).some((e) => /non-public\/reserved/.test(e)),
398+
`expected ${origin} to stay refused`,
399+
);
400+
}
401+
});

0 commit comments

Comments
 (0)