mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 00:52:18 +03:00
fix(authz): restrict tunnel process routes locally (#11531)
Validated in a combined 10-PR batch worktree off release/v3.8.51 tip. - Focused tests: tests/unit/authz/route-guard-tunnel-processes-local-only.test.ts + tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts — 6+ pass - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK - Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip — unrelated to this diff ⚠️ base-red inherited: #11449 Thanks for closing the process-spawning tunnel routes to local-only (Hard Rule #15/#17 territory) while preserving authenticated remote read access to status endpoints.
This commit is contained in:
1
changelog.d/fixes/tunnel-process-routes-local-only.md
Normal file
1
changelog.d/fixes/tunnel-process-routes-local-only.md
Normal file
@@ -0,0 +1 @@
|
||||
- Classify Cloudflared and Tailscale process-management routes as local-only and spawn-capable while preserving remote access to read-only tunnel status endpoints.
|
||||
@@ -40,6 +40,12 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
|
||||
"/api/cli-tools/jcode-settings", // spawns via getCliRuntimeStatus() to detect the `jcode` CLI install (Hard Rules #15 + #17, #7263)
|
||||
"/api/cli-tools/qwen-settings", // GET probes the local `qwen` binary; writes target ~/.qwen config files (Hard Rules #15 + #17)
|
||||
"/api/services/", // T-10: embedded service lifecycle (spawn child processes)
|
||||
"/api/tunnels/cloudflared", // POST installs/starts/stops cloudflared; safe methods are exempted below
|
||||
"/api/tunnels/tailscale/disable", // stops Funnel and may stop tailscaled/Tailscale service
|
||||
"/api/tunnels/tailscale/enable", // starts tailscaled/login/funnel subprocesses
|
||||
"/api/tunnels/tailscale/install", // downloads/installs Tailscale and starts its daemon
|
||||
"/api/tunnels/tailscale/login", // spawns `tailscale up`
|
||||
"/api/tunnels/tailscale/start-daemon", // starts tailscaled/Tailscale service
|
||||
"/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)
|
||||
@@ -213,7 +219,10 @@ export function isPrivateLanHost(hostHeader: string | null): boolean {
|
||||
* triggers the auto-update flow (spawns git checkout + npm install + pm2).
|
||||
* Hard Rules #15/#17 still apply to POST.
|
||||
*/
|
||||
export const LOCAL_ONLY_API_GET_EXEMPTIONS: ReadonlySet<string> = new Set(["/api/system/version"]);
|
||||
export const LOCAL_ONLY_API_GET_EXEMPTIONS: ReadonlySet<string> = new Set([
|
||||
"/api/system/version",
|
||||
"/api/tunnels/cloudflared",
|
||||
]);
|
||||
|
||||
/** Safe HTTP methods that can be exempted for read-only paths. */
|
||||
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
||||
|
||||
@@ -27,6 +27,12 @@ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray<string> = [
|
||||
"/api/cli-tools/runtime/",
|
||||
"/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/tunnels/cloudflared", // POST installs/starts/stops cloudflared; safe methods remain read-only exempt
|
||||
"/api/tunnels/tailscale/disable", // stops Funnel and may stop tailscaled/Tailscale service
|
||||
"/api/tunnels/tailscale/enable", // starts tailscaled/login/funnel subprocesses
|
||||
"/api/tunnels/tailscale/install", // downloads/installs Tailscale and starts its daemon
|
||||
"/api/tunnels/tailscale/login", // spawns `tailscale up`
|
||||
"/api/tunnels/tailscale/start-daemon", // starts tailscaled/Tailscale service
|
||||
"/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)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
LOCAL_ONLY_API_GET_EXEMPTIONS,
|
||||
LOCAL_ONLY_API_PREFIXES,
|
||||
isLocalOnlyPath,
|
||||
} from "../../../src/server/authz/routeGuard.ts";
|
||||
import { SPAWN_CAPABLE_PREFIXES } from "../../../src/shared/constants/spawnCapablePrefixes.ts";
|
||||
|
||||
const PROCESS_ROUTES = [
|
||||
"/api/tunnels/cloudflared",
|
||||
"/api/tunnels/tailscale/disable",
|
||||
"/api/tunnels/tailscale/enable",
|
||||
"/api/tunnels/tailscale/install",
|
||||
"/api/tunnels/tailscale/login",
|
||||
"/api/tunnels/tailscale/start-daemon",
|
||||
] as const;
|
||||
|
||||
test("tunnel process routes are local-only and non-bypassable", () => {
|
||||
for (const path of PROCESS_ROUTES) {
|
||||
assert.ok(LOCAL_ONLY_API_PREFIXES.includes(path), `${path} must be explicitly local-only`);
|
||||
assert.ok(
|
||||
SPAWN_CAPABLE_PREFIXES.includes(path),
|
||||
`${path} must be denied from the manage-scope bypass`
|
||||
);
|
||||
assert.equal(isLocalOnlyPath(path, "POST"), true, `${path} POST must be local-only`);
|
||||
}
|
||||
});
|
||||
|
||||
test("cloudflared status remains remotely available for authenticated callers", () => {
|
||||
assert.ok(LOCAL_ONLY_API_GET_EXEMPTIONS.has("/api/tunnels/cloudflared"));
|
||||
assert.equal(isLocalOnlyPath("/api/tunnels/cloudflared", "GET"), false);
|
||||
assert.equal(isLocalOnlyPath("/api/tunnels/cloudflared", "HEAD"), false);
|
||||
assert.equal(isLocalOnlyPath("/api/tunnels/cloudflared", "OPTIONS"), false);
|
||||
assert.equal(isLocalOnlyPath("/api/tunnels/cloudflared", "POST"), true);
|
||||
});
|
||||
|
||||
test("Tailscale read-only status routes are not broadened into local-only", () => {
|
||||
for (const path of ["/api/tunnels/tailscale", "/api/tunnels/tailscale/check"]) {
|
||||
assert.equal(LOCAL_ONLY_API_PREFIXES.includes(path), false);
|
||||
assert.equal(SPAWN_CAPABLE_PREFIXES.includes(path), false);
|
||||
assert.equal(isLocalOnlyPath(path, "GET"), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("unrelated tunnel paths remain outside the spawn-capable classification", () => {
|
||||
for (const path of ["/api/tunnels/ngrok", "/api/tunnels/tailscale/status"]) {
|
||||
assert.equal(LOCAL_ONLY_API_PREFIXES.includes(path), false);
|
||||
assert.equal(SPAWN_CAPABLE_PREFIXES.includes(path), false);
|
||||
assert.equal(isLocalOnlyPath(path, "GET"), false);
|
||||
}
|
||||
});
|
||||
@@ -84,11 +84,17 @@ test("SPAWN_CAPABLE_PREFIXES is defined in the server-free constants leaf with t
|
||||
"/api/modality-bridge/video/",
|
||||
"/api/settings/mitm",
|
||||
"/api/cli-tools/antigravity-mitm",
|
||||
"/api/tunnels/cloudflared",
|
||||
"/api/tunnels/tailscale/disable",
|
||||
"/api/tunnels/tailscale/enable",
|
||||
"/api/tunnels/tailscale/install",
|
||||
"/api/tunnels/tailscale/login",
|
||||
"/api/tunnels/tailscale/start-daemon",
|
||||
]) {
|
||||
assert.ok(
|
||||
SPAWN_CAPABLE_PREFIXES.includes(prefix),
|
||||
`SPAWN_CAPABLE_PREFIXES lost the spawn-capable prefix "${prefix}" during extraction`
|
||||
);
|
||||
}
|
||||
assert.equal(SPAWN_CAPABLE_PREFIXES.length, 14);
|
||||
assert.equal(SPAWN_CAPABLE_PREFIXES.length, 20);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user