diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index 7c61d24545..c5cbe01501 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -43,6 +43,8 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray = [ "/dashboard/providers/services/", // T-07: reverse proxy to embedded service UIs "/api/copilot/", // unauthenticated LLM driver — CLI-only by default; admins can opt-in to remote access via manage-scope bypass "/api/tools/agent-bridge/", // AgentBridge: spawns MITM server + DNS edits (Hard Rules #15 + #17) + "/api/settings/mitm", // "Enable MITM" flow: installs a system-wide trusted root CA (security add-trusted-cert / certutil / update-ca-certificates) and writes /etc/hosts DNS overrides via src/mitm/* — host-level TLS interception. Was MANAGEMENT-only, so requireLogin=false left it remotely reachable (GHSA-x7vm-hp44-9p79, Hard Rules #15 + #17). Same tier as /api/tools/agent-bridge/. + "/api/cli-tools/antigravity-mitm", // Antigravity MITM enable flow: same privileged CA-trust + DNS surface as /api/settings/mitm (GHSA-x7vm-hp44-9p79, Hard Rules #15 + #17). Covers the /alias child route by prefix. "/api/tools/traffic-inspector/", // Traffic Inspector: http-proxy listener + system proxy (Hard Rules #15 + #17) "/api/issue-agent/", // Issue Agent: recorded/local triage executor surface; keep loopback/LAN until sandbox + audit hardening is complete "/api/plugins/", // plugins: load/execute via worker_threads + child_process (Hard Rules #15 + #17) @@ -126,6 +128,12 @@ export const ALWAYS_PROTECTED_API_PATHS: ReadonlyArray = [ // /api/settings/database already does. isAlwaysProtectedPath matches on a path // boundary, so this covers export, exportAll and import. (GHSA-mghq-58h3-qcqj) "/api/db-backups", + // Legacy siblings of /api/db-backups left out of the mghq fix: export-json + // dumps every stored credential and import-json irreversibly replaces + // settings/connections, and both handlers only gate on isAuthRequired() — + // which is false under requireLogin=false. (GHSA-v7g9-7f55-5g46) + "/api/settings/export-json", + "/api/settings/import-json", ]; export function isLoopbackHost(hostHeader: string | null): boolean { diff --git a/src/shared/constants/spawnCapablePrefixes.ts b/src/shared/constants/spawnCapablePrefixes.ts index 20787d7d2f..3a1a05a881 100644 --- a/src/shared/constants/spawnCapablePrefixes.ts +++ b/src/shared/constants/spawnCapablePrefixes.ts @@ -28,6 +28,8 @@ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray = [ "/api/cli-tools/qwen-settings", // GET probes the Qwen Code binary; the route also mutates local ~/.qwen files "/api/services/", // T-10: can run npm install + spawn node processes "/api/tools/agent-bridge/", // start/stop MITM server + DNS edits (Hard Rules #15 + #17) + "/api/settings/mitm", // installs a system trusted root CA + /etc/hosts DNS overrides via src/mitm/* — must never be whitelistable via manage-scope bypass (GHSA-x7vm-hp44-9p79, Hard Rules #15 + #17) + "/api/cli-tools/antigravity-mitm", // same privileged CA-trust + DNS surface as /api/settings/mitm (GHSA-x7vm-hp44-9p79, Hard Rules #15 + #17) "/api/tools/traffic-inspector/", // http-proxy listener + system proxy (Hard Rules #15 + #17) "/api/plugins/", // plugins: load/execute via worker_threads + child_process (Hard Rules #15 + #17) "/api/local/", // T-12: 1-click local service launchers (Redis today) — must never be whitelistable via manage-scope bypass (Hard Rules #15 + #17) diff --git a/tests/unit/authz/routeGuard.test.ts b/tests/unit/authz/routeGuard.test.ts index 163f5bce41..cae8ef4a8c 100644 --- a/tests/unit/authz/routeGuard.test.ts +++ b/tests/unit/authz/routeGuard.test.ts @@ -22,6 +22,22 @@ test("isLocalOnlyPath: /api/cli-tools/runtime/ is local-only", () => { assert.equal(isLocalOnlyPath("/api/cli-tools/runtime/claude"), true); }); +test("isLocalOnlyPath: MITM management routes are local-only (GHSA-x7vm-hp44-9p79)", () => { + // The "Enable MITM" flow installs a system-wide trusted root CA and writes + // /etc/hosts DNS overrides (src/mitm/*) — host-level TLS interception. Both + // routes were MANAGEMENT-classified only, so requireLogin=false left them + // remotely reachable. They belong to the same loopback tier as + // /api/tools/agent-bridge/ (also MITM + DNS). + assert.equal(isLocalOnlyPath("/api/settings/mitm"), true); + assert.equal(isLocalOnlyPath("/api/cli-tools/antigravity-mitm"), true); + assert.equal(isLocalOnlyPath("/api/cli-tools/antigravity-mitm/alias"), true); +}); + +test("isLocalOnlyBypassableByManageScope: MITM routes are NOT bypassable (GHSA-x7vm-hp44-9p79)", () => { + assert.equal(isLocalOnlyBypassableByManageScope("/api/settings/mitm"), false); + assert.equal(isLocalOnlyBypassableByManageScope("/api/cli-tools/antigravity-mitm"), false); +}); + test("isLocalOnlyPath: regular management routes are not local-only", () => { assert.equal(isLocalOnlyPath("/api/settings"), false); assert.equal(isLocalOnlyPath("/api/providers"), false); @@ -89,6 +105,19 @@ test("isAlwaysProtectedPath: /api/db-backups is always protected (GHSA-mghq-58h3 assert.equal(isAlwaysProtectedPath("/api/db-backups/import"), true); }); +test("isAlwaysProtectedPath: legacy settings export/import-json are always protected (GHSA-v7g9-7f55-5g46)", () => { + // The mghq fix covered /api/db-backups but left the legacy sibling routes out: + // export-json dumps every credential and import-json irreversibly replaces + // settings/connections. Both handlers only check isAuthRequired(), which + // returns false under requireLogin=false — so they must sit in Tier 2 like + // /api/settings/database and /api/db-backups. + assert.equal(isAlwaysProtectedPath("/api/settings/export-json"), true); + assert.equal(isAlwaysProtectedPath("/api/settings/import-json"), true); + // The matcher is a plain startsWith (fail-closed: covers more, never less), + // so a hypothetical export-json2 sibling would also be protected — fine. + assert.equal(isAlwaysProtectedPath("/api/settings/proxy"), false); +}); + test("isAlwaysProtectedPath: ordinary settings routes are not always protected", () => { assert.equal(isAlwaysProtectedPath("/api/settings"), false); assert.equal(isAlwaysProtectedPath("/api/settings/proxy"), false); diff --git a/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts index 788777596d..8a2c6b9e17 100644 --- a/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts +++ b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts @@ -82,11 +82,13 @@ test("SPAWN_CAPABLE_PREFIXES is defined in the server-free constants leaf with t "/api/headroom/stop", "/api/vnc-session", "/api/modality-bridge/video/", + "/api/settings/mitm", + "/api/cli-tools/antigravity-mitm", ]) { assert.ok( SPAWN_CAPABLE_PREFIXES.includes(prefix), `SPAWN_CAPABLE_PREFIXES lost the spawn-capable prefix "${prefix}" during extraction` ); } - assert.equal(SPAWN_CAPABLE_PREFIXES.length, 12); + assert.equal(SPAWN_CAPABLE_PREFIXES.length, 14); });