mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 08:02:14 +03:00
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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user