mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
fix(oauth): classify /api/oauth/cursor/auto-import as local-only (route-guard) (#5070)
The Cursor auto-import route runs execFile("which", ["cursor"]) to verify a
local Cursor install before importing credentials — a child-process spawn. The
check:route-guard-membership gate (Hard Rules #15/#17) flagged it as an
unclassified spawn-capable route: reachable past the loopback gate, an
RCE-via-tunnel surface (a leaked JWT over a tunnel could trigger the spawn).
Classify the specific path in LOCAL_ONLY_API_PREFIXES so loopback enforcement
runs unconditionally before any auth check. Scoped to the exact path — the rest
of /api/oauth/ (browser redirect/callback flows) stays remote-reachable.
TDD: added a failing-then-passing assertion in route-guard-local-prefix.test.ts
(classification + an over-broadening guard proving sibling OAuth paths stay
remote). check:route-guard-membership now reports 0 new gaps.
This commit is contained in:
committed by
GitHub
parent
2e5a0e03a5
commit
749009736e
@@ -40,6 +40,7 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
|
||||
"/api/local/", // T-12: 1-click local service launchers (Redis today; spawns podman/docker) — loopback-enforced by isLocalRequestAllowed() in src/lib/security/localEndpoints.ts (Hard Rules #15 + #17)
|
||||
"/api/headroom/start", // Headroom token-saver proxy lifecycle: spawns headroom-ai python CLI (Hard Rules #15 + #17)
|
||||
"/api/headroom/stop", // Headroom token-saver proxy lifecycle: sends SIGTERM/SIGKILL to managed PID (Hard Rules #15 + #17)
|
||||
"/api/oauth/cursor/auto-import", // spawns `execFile("which", ["cursor"])` to verify a local Cursor install before importing creds — RCE-via-tunnel surface (Hard Rules #15 + #17, found by 6A.8 route-guard gate). Specific path only: the rest of /api/oauth/ (browser redirect/callback flows) must stay remote-reachable.
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,25 @@ test("isLocalOnlyPath: /api/local* does NOT match the bare /api/localifications
|
||||
assert.equal(isLocalOnlyPath("/api/localhost-check"), false);
|
||||
});
|
||||
|
||||
// ─── /api/oauth/cursor/auto-import is local-only (spawns `which cursor`) ──
|
||||
|
||||
test("isLocalOnlyPath: /api/oauth/cursor/auto-import is local-only (spawns child process)", () => {
|
||||
// The Cursor auto-import route runs `execFile("which", ["cursor"])` to verify a
|
||||
// local Cursor install before importing its credentials — a child-process spawn
|
||||
// that must be loopback-enforced BEFORE any auth check (Hard Rules #15/#17), so a
|
||||
// leaked JWT via tunnel cannot reach the spawn. Found by check:route-guard-membership.
|
||||
assert.equal(isLocalOnlyPath("/api/oauth/cursor/auto-import"), true);
|
||||
});
|
||||
|
||||
test("isLocalOnlyPath: the rest of /api/oauth/ stays remote-reachable (no over-broadening)", () => {
|
||||
// Only the spawn-capable auto-import path is loopback-locked. The rest of the OAuth
|
||||
// surface (browser redirect / callback flows) MUST remain reachable remotely — a
|
||||
// flat /api/oauth/ prefix would wrongly lock the whole OAuth subtree.
|
||||
assert.equal(isLocalOnlyPath("/api/oauth/cursor"), false);
|
||||
assert.equal(isLocalOnlyPath("/api/oauth/cursor/callback"), false);
|
||||
assert.equal(isLocalOnlyPath("/api/oauth/anthropic/callback"), false);
|
||||
});
|
||||
|
||||
test("isLocalOnlyBypassableByManageScope: /api/local/ is NOT bypassable (defence in depth)", () => {
|
||||
// The kill-switch path. Even if a DB row tries to whitelist /api/local/ via
|
||||
// the manage-scope bypass list, the runtime predicate must reject it because
|
||||
|
||||
Reference in New Issue
Block a user