Refuse the URL-repaired spellings in the protection gate too

`isLoopbackEndpoint` hands its endpoint to `refusedByCSharpUriShape` before
`URL` sees it; `isProtectedEndpoint` did not, and `URL` repairs eleven
spellings `System.Uri` refuses outright. Every one of them answered protected
here and unprotected in the C# reference, which is the dangerous direction:
the channel-protection gate opened on a route `IsProtected` would have closed.
`wss:x`, `wss:host`, `wss:host:8443`, `wss:/host` and `wss:///host` have no
authority for `System.Uri`; `wss://host\path` and `wss://host:8443\` carry a
backslash; `wss://ho\tst/m`, `wss://host\t/m` and `quic://ho\tst` a tab, and
`wss://host\r\n/m` a CR/LF, all of which `URL` deletes.

Soul's 71 raw-shape spellings go in as a table with the answer the C#
`IsProtected` gave for each on 2026-09-16, and the test asserts the invariant
rather than equality: TypeScript may never call an endpoint protected that C#
refuses. Eleven of the 71 now differ the safe way round, C# protected and this
not, and they are pinned by name rather than excluded, so neither a twelfth
joining them nor one of them flipping passes quietly. Two throw in `URL` before
any rule of ours runs (an IPv6 zone id, a non-breaking-space host); the other
nine are host repairs `refusedByCSharpUriShape` refuses on purpose, because the
same repair would make `isLoopbackEndpoint` lie. None is dialable as a browser
CultMesh route.

The mutation that drops the pre-check from `isProtectedEndpoint` alone, leaving
`isLoopbackEndpoint`'s in place, is killed by that test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 18:29:13 +02:00
commit c64a2da81d
3 changed files with 147 additions and 1 deletions

View file

@ -361,8 +361,28 @@ export function isLoopbackEndpoint(value: string): boolean {
}
}
/** The C# rule (`CultMeshAuthorityProof.cs`, `IsProtected`): wss, https, or any scheme containing "quic". */
/**
* The C# rule (`CultMeshAuthorityProof.cs`, `IsProtected`): wss, https, or any
* scheme containing "quic", over a string `System.Uri` parses at all.
*
* `refusedByCSharpUriShape` runs first here for the same reason it runs first in
* `isLoopbackEndpoint`, and it matters more: `URL` repairs eleven spellings
* `System.Uri` refuses outright, and every one of them would be *protected* here
* and unprotected in C#. `wss:x`, `wss:host`, `wss:host:8443`, `wss:/host` and
* `wss:///host` have no authority for `System.Uri` to accept; `wss://host\path`
* and `wss://host:8443\` carry a backslash; `wss://ho\tst/m`, `wss://host\t/m`,
* `quic://ho\tst` carry a tab and `wss://host\r\n/m` a CR/LF, which `URL`
* deletes. Letting any of them answer true would clear the channel-protection
* gate on a route C# refuses.
*
* Two spellings of Soul's 71 still disagree, both the safe way round: C# calls
* them protected and this answers false, so a route is refused rather than
* admitted. `wss://[::1%eth0]/m` (the IPv6 zone id) and `wss://host /m`
* (a non-breaking space in the host) both throw in `URL`, so they answered
* false before this pre-check existed too. Neither is dialable from a browser.
*/
export function isProtectedEndpoint(value: string): boolean {
if (refusedByCSharpUriShape(value)) return false;
let scheme: string;
try {
scheme = new URL(value).protocol.slice(0, -1).toLowerCase();

View file

@ -311,6 +311,124 @@ test("isProtectedEndpoint is the C# rule: wss, https, or any scheme containing q
}
});
// Every raw-shape spelling Soul put to both sides on 2026-09-16, with the answer
// `IsProtected` gave through `CultMeshAuthorityTrustPolicy` as the expectation.
// The list is Soul's 71 verbatim, duplicate included.
const CSHARP_PROTECTED_ANSWERS: readonly (readonly [string, boolean])[] = [
["ws:x", false],
["wss:x", false],
["wss:/host", false],
["wss:///host", false],
["wss://host:8443\\", false],
["wss://host\\path", false],
["wss:\\\\host/m", true],
["wss://host./m", true],
["wss://\uff48\uff4f\uff53\uff54/m", true],
["QUIC://host/m", true],
["Quic+x://host/m", true],
["cultmesh-state+quic://h:4433/?c=1", true],
["wss://ho\tst/m", false],
["wss://host\t/m", false],
["\twss://host/m", true],
[" wss://host/m", true],
["wss://host/m ", true],
["wss://host/m\n", true],
["wss://host:99999/m", false],
["wss://[::1%eth0]/m", true],
["wss://user:pw@host/m", true],
["https://host/m", true],
["HTTPS://host", true],
["ws://host/m", false],
["WS://host/m", false],
["http://host/m", false],
["quic:host", true],
["quic:", true],
["wss:", false],
["wss://", false],
["wss:///", false],
["quic+wss://host", true],
["x-quic://host", true],
["xquicx://host", true],
["wss ://host/m", false],
["w ss://host/m", false],
["wss://host:abc/m", false],
["wss://\uff11\uff12\uff17.0.0.1/m", true],
["wss://127.0.0.1./m", true],
["not a url", false],
["", false],
[" ", false],
["wss://host?x=1", true],
["wss://host#f", true],
["wss://[::1]/m", true],
["wss://[::1", false],
["wss://host/m\u0000", true],
["wss://host/\u0085m", true],
["\u0085wss://host/m", false],
["\ufeffwss://host/m", false],
["ws://127.0.0.1\\m", false],
["ws:127.0.0.1", false],
["ws:/127.0.0.1", false],
["ws:///127.0.0.1", false],
["ws://127.0.0.1./m", false],
["ws://\uff4c\uff4f\uff43\uff41\uff4c\uff48\uff4f\uff53\uff54/m", false],
["ws://localhost\t/m", false],
["wss://host:8443\\", false],
["wss:host", false],
["wss:host:8443", false],
["wss://host:8443/\\", true],
["wss://ho st/m", false],
["wss://host%20/m", false],
["wss://host\u00a0/m", true],
["wss://host\r\n/m", false],
["wss:// host/m", false],
["wss://host /m", false],
["quic://ho\tst", false],
["QUIC:\\\\host", true],
["ws://127.0.0.1:1/m\t", false],
["wss://xn--80ak6aa92e.com/m", true],
];
// The eleven of the 71 where the two sides still differ, every one of them the
// safe way round: C# calls the endpoint protected and TypeScript does not, so a
// route is refused rather than admitted. `refusedByCSharpUriShape` is stricter
// than `System.Uri.TryCreate` on purpose — it also refuses the host repairs that
// would make `isLoopbackEndpoint` lie — and a zone id and a non-breaking-space
// host throw in `URL` before any rule of ours runs. None of them is dialable as
// a browser CultMesh route. Pinned exactly, not merely excluded: if one of these
// starts answering true, or a twelfth spelling joins them, this fails.
const STRICTER_THAN_CSHARP: readonly string[] = [
"wss:\\\\host/m",
"wss://host./m",
"wss://\uff48\uff4f\uff53\uff54/m",
"wss://[::1%eth0]/m",
"quic:host",
"quic:",
"wss://\uff11\uff12\uff17.0.0.1/m",
"wss://127.0.0.1./m",
"wss://host:8443/\\",
"wss://host\u00a0/m",
"QUIC:\\\\host",
];
test("isProtectedEndpoint is never more permissive than the C# IsProtected, over every raw-shape spelling Soul probed", () => {
assert.equal(CSHARP_PROTECTED_ANSWERS.length, 71);
const stricter = new Set(STRICTER_THAN_CSHARP);
// The invariant that matters: TypeScript must never call an endpoint protected
// that C# refuses, or the channel-protection gate opens on a route the
// reference would have closed. `URL` repairs eleven spellings `System.Uri`
// refuses outright, and every one of them used to land on the wrong side here.
const morePermissive: string[] = [];
const unexpectedlyStricter: string[] = [];
for (const [endpoint, csharp] of CSHARP_PROTECTED_ANSWERS) {
const ours = isProtectedEndpoint(endpoint);
if (ours && !csharp) morePermissive.push(JSON.stringify(endpoint));
if (!ours && csharp && !stricter.has(endpoint)) unexpectedlyStricter.push(JSON.stringify(endpoint));
if (stricter.has(endpoint)) assert.equal(ours, false, `${JSON.stringify(endpoint)} is listed as stricter`);
}
assert.deepEqual(morePermissive, []);
assert.deepEqual(unexpectedlyStricter, []);
});
// Every endpoint Soul put to both sides on 2026-09-16, with the answer
// `System.Uri.IsLoopback` gave through `CultMeshAuthorityTrustPolicy` as the
// expectation. Six of the probe's 88 are left out and named in the doc comment

View file

@ -233,6 +233,14 @@ const mutations = [
old: 'scheme.includes("quic")',
new: 'scheme === "quic"',
},
{
// Only `isProtectedEndpoint`'s copy: `isLoopbackEndpoint` keeps its own, so
// this pins that the protection gate refuses the spellings `URL` repairs
// rather than inheriting the answer from the loopback rule.
rule: "isProtectedEndpoint refuses the raw shapes System.Uri refuses",
old: " if (refusedByCSharpUriShape(value)) return false;\n let scheme: string;\n",
new: " let scheme: string;\n",
},
{
rule: "protected scheme: https",
old: 'scheme === "https" || ',