fix(security): add chatgpt-web-codex-doctor to SPAWN_CAPABLE_PATTERNS

The manage-scope bypass veto's precise early-deny keys on
SPAWN_CAPABLE_PATTERNS, but /api/providers/{id}/chatgpt-web-codex-doctor — a
LOCAL_ONLY route that spawns a subprocess via getTunnelRuntimeStatus() — was in
LOCAL_ONLY_API_PATTERNS without a matching spawn-capable pattern, so the two
layers had drifted. Add the pattern plus a regression test asserting every
regex-tier LOCAL_ONLY spawn route is covered, so the veto's exact early-deny
stays in sync with the tier.

Reported by @Zandereins via GHSA-9q3h-mjm5-f4gj (finding 1).
This commit is contained in:
Xiangzhe
2026-08-21 13:23:01 -03:00
parent a2d5ef50f4
commit be1a3ea778
2 changed files with 23 additions and 0 deletions

View File

@@ -52,6 +52,7 @@ export const SPAWN_CAPABLE_PATTERNS: ReadonlyArray<RegExp> = [
/^\/api\/providers\/[^/]+\/login\/?$/, // pre-existing gap: in LOCAL_ONLY_API_PATTERNS today but never in a spawn-capable deny-list
/^\/api\/providers\/[^/]+\/refresh-cursor\/?$/, // spawns cursor-agent via renewal.ts (Hard Rules #15 + #17)
/^\/api\/providers\/cursor\/agent-availability\/?$/, // static path (no dynamic segment), but kept in this array alongside its /api/providers/ siblings rather than the flat SPAWN_CAPABLE_PREFIXES array — spawns cursor-agent status via checkCursorAgentAvailability()/getCachedCursorAgentAvailability() (Hard Rules #15 + #17)
/^\/api\/providers\/[^/]+\/chatgpt-web-codex-doctor\/?$/, // spawns via getTunnelRuntimeStatus() → spawnSync("...","runtimes status") (open-sse/executors/chatgpt-web-codex/tunnelClient.ts). Mirrors LOCAL_ONLY_API_PATTERNS in routeGuard.ts; keep the two in sync (GHSA-9q3h-mjm5-f4gj).
];
/**

View File

@@ -6,6 +6,7 @@ import {
isAlwaysProtectedPath,
isLoopbackHost,
} from "../../../src/server/authz/routeGuard.ts";
import { SPAWN_CAPABLE_PATTERNS } from "../../../src/shared/constants/spawnCapablePrefixes.ts";
import { managementPolicy } from "../../../src/server/authz/policies/management.ts";
import { getMachineTokenSync } from "../../../src/lib/machineToken.ts";
import { CLI_TOKEN_HEADER } from "../../../src/server/authz/headers.ts";
@@ -50,6 +51,27 @@ test("isLocalOnlyBypassableByManageScope: non-local-only routes are not bypassab
assert.equal(isLocalOnlyBypassableByManageScope("/api/settings"), false);
});
test("SPAWN_CAPABLE_PATTERNS covers every regex-tier LOCAL_ONLY spawn route (GHSA-9q3h-mjm5-f4gj)", () => {
// The manage-scope bypass veto's precise early-deny keys on
// SPAWN_CAPABLE_PATTERNS, so every LOCAL_ONLY_API_PATTERNS entry (a
// spawn-capable regex route) must have a matching pattern here — otherwise the
// two layers drift and a spawn route loses its exact early-deny. This guards
// against the chatgpt-web-codex-doctor drift and any future one.
const spawnRoutes = [
"/api/providers/acct-1/login",
"/api/providers/acct-1/refresh-cursor",
"/api/providers/acct-1/chatgpt-web-codex-doctor",
];
for (const p of spawnRoutes) {
assert.equal(isLocalOnlyPath(p), true, `${p} must be LOCAL_ONLY`);
assert.equal(
SPAWN_CAPABLE_PATTERNS.some((re) => re.test(p)),
true,
`${p} is a LOCAL_ONLY spawn route but SPAWN_CAPABLE_PATTERNS does not cover it`
);
}
});
test("isAlwaysProtectedPath: /api/shutdown is always protected", () => {
assert.equal(isAlwaysProtectedPath("/api/shutdown"), true);
});