fix(security): harden authz tiers — legacy export/import + MITM loopback

GHSA-v7g9-7f55-5g46 (follow-up to mghq): /api/settings/export-json dumps every
stored credential and /api/settings/import-json irreversibly replaces settings,
yet both were left out of the mghq ALWAYS_PROTECTED fix and their handlers only
gate on isAuthRequired() — false under requireLogin=false. Added to
ALWAYS_PROTECTED_API_PATHS alongside /api/settings/database and /api/db-backups.

GHSA-x7vm-hp44-9p79: the MITM management routes (/api/settings/mitm,
/api/cli-tools/antigravity-mitm) install a system-wide trusted root CA and
write /etc/hosts DNS overrides, but were MANAGEMENT-only — remotely reachable
under requireLogin=false, violating the documented loopback contract for
privileged surfaces (Hard Rules #15/#17). Added to LOCAL_ONLY_API_PREFIXES and
SPAWN_CAPABLE_PREFIXES (never manage-scope bypassable), same tier as
/api/tools/agent-bridge/.
This commit is contained in:
Xiangzhe
2026-08-23 12:46:50 -03:00
parent 0b41259f39
commit e1fdfc40a5
4 changed files with 42 additions and 1 deletions

View File

@@ -43,6 +43,8 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
"/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<string> = [
// /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 {

View File

@@ -28,6 +28,8 @@ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray<string> = [
"/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)

View File

@@ -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);

View File

@@ -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);
});