test(authz): stop skipping the loopback coverage and re-point the Host-spoofing guard

Two follow-ups to the peerContext extraction, both caught by CI.

public-policy.test.ts guarded its two loopback cases behind
`if (!getMachineTokenSync()) t.skip(...)`. Those are the only tests covering the
branch the pack-boot fix added, so a machine-id that failed to resolve would have
silenced exactly the coverage that matters and shipped the authorization change
untested — which is what the test-masking detector flagged. The sibling
management-policy cases in authz/routeGuard.test.ts call the same helper with no
guard and pass in CI, so an empty token is a broken environment: assert on it
loudly instead of skipping.

route-guard-private-lan.test.ts pinned its Host-spoofing regression guard to
source text inside management.ts; moving requestPeerAddress into peerContext.ts
made the literals disappear from the file it greps, while the property itself was
untouched. The guard now follows the implementation: neither module may read the
Host header, the owning module must resolve the token-stamped peer, and — new
positive anchor — management.ts must still delegate to peerContext, so the guard
cannot pass by the policy quietly regrowing a Host-based path.
This commit is contained in:
Xiangzhe
2026-08-26 08:00:45 -03:00
parent 7448780cbd
commit 031e7659b2
2 changed files with 35 additions and 15 deletions

View File

@@ -47,11 +47,13 @@ function cliCtx(overrides: Partial<PolicyContext["request"]> = {}): PolicyContex
};
}
test("publicPolicy stamps the local-CLI subject for a valid loopback machine token", async (t) => {
if (!getMachineTokenSync()) {
t.skip("machine-id unavailable in this environment");
return;
}
test("publicPolicy stamps the local-CLI subject for a valid loopback machine token", async () => {
// No environment guard here on purpose: this and the negative control below are
// the only tests covering the loopback branch added for check:pack-boot, and a
// conditional skip would silence them exactly where the coverage matters. The
// sibling management-policy tests in authz/routeGuard.test.ts call this same
// helper unguarded, so an empty token is a broken environment worth failing on.
assert.ok(getMachineTokenSync(), "machine token must resolve for this suite to mean anything");
const out = await publicPolicy.evaluate(cliCtx());
assert.equal(out.allow, true);
if (out.allow) {
@@ -60,11 +62,8 @@ test("publicPolicy stamps the local-CLI subject for a valid loopback machine tok
}
});
test("publicPolicy keeps anonymous for a non-loopback peer carrying the token", async (t) => {
if (!getMachineTokenSync()) {
t.skip("machine-id unavailable in this environment");
return;
}
test("publicPolicy keeps anonymous for a non-loopback peer carrying the token", async () => {
assert.ok(getMachineTokenSync(), "machine token must resolve for this suite to mean anything");
const out = await publicPolicy.evaluate(cliCtx({ ip: "203.0.113.7" }));
assert.equal(out.allow, true);
if (out.allow) assert.equal(out.subject.kind, "anonymous");

View File

@@ -95,17 +95,38 @@ test("management policy must NOT derive locality from the spoofable Host header"
join(import.meta.dirname, "../../src/server/authz/policies/management.ts"),
"utf8"
);
// `requestPeerAddress` and friends moved to authz/peerContext.ts when the PUBLIC
// policy had to share the very same verdict (check:pack-boot / #11040 follow-up).
// The guard follows the implementation instead of the filename: neither module may
// read the Host header, and the module that OWNS peer resolution must resolve the
// token-stamped peer IP.
const peerSrc = readFileSync(
join(import.meta.dirname, "../../src/server/authz/peerContext.ts"),
"utf8"
);
// Regression guard: a prior fix read the client-controlled Host header for the
// LOCAL_ONLY decision, letting `Host: 127.0.0.1` bypass the gate. Locality must
// come from the token-stamped peer IP instead.
for (const [name, text] of [
["management.ts", src],
["peerContext.ts", peerSrc],
] as const) {
assert.ok(
!text.includes('get?.("host")') && !text.includes('get("host")'),
`${name} must NOT read the Host header for locality`
);
}
assert.ok(
!src.includes('get?.("host")') && !src.includes('get("host")'),
"requestPeerAddress must NOT read the Host header"
);
assert.ok(
src.includes("resolveStampedPeer") && src.includes("PEER_IP_HEADER"),
peerSrc.includes("resolveStampedPeer") && peerSrc.includes("PEER_IP_HEADER"),
"requestPeerAddress must resolve the trusted token-stamped peer IP"
);
// Positive anchor: management.ts must still route its locality decision through
// the shared helpers, so this guard cannot pass by the policy quietly growing its
// own Host-based path again.
assert.ok(
src.includes("peerContext") && src.includes("isLoopbackRequest"),
"management policy must delegate locality to authz/peerContext"
);
});
// ── resolveStampedPeer: the auth boundary that replaces Host-header trust ──