Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/server/authz/routeGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
];

/**
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/authz/route-guard-local-prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading