fix(authz): classify forge/jcode CLI settings routes as LOCAL_ONLY (#7263) (#7749)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-19 09:39:13 -03:00
committed by GitHub
parent ded4ac830e
commit a19f86b8ca
4 changed files with 53 additions and 0 deletions

View File

@@ -0,0 +1 @@
- fix(authz): classify /api/cli-tools/forge-settings and /api/cli-tools/jcode-settings as LOCAL_ONLY, closing an RCE-via-tunnel gap where getCliRuntimeStatus() spawns a child process without loopback enforcement (#7263)

View File

@@ -50,6 +50,8 @@ export const SPAWN_CAPABLE_ROUTE_ROOTS: ReadonlyArray<string> = [
"src/app/api/cli-tools/runtime",
"src/app/api/local", // T-12: 1-click local service launchers (Redis today) — every child here spawns podman/docker (Hard Rules #15 + #17)
"src/app/api/skills/collect", // Skill Collector CLI detection: GET .../detect spawns a child process per CLI_TOOL_IDS entry via getCliRuntimeStatus() (Hard Rules #15 + #17, PR #6294 review)
"src/app/api/cli-tools/forge-settings", // GET calls getCliRuntimeStatus() to detect the `forge` CLI install (Hard Rules #15 + #17, #7263)
"src/app/api/cli-tools/jcode-settings", // GET calls getCliRuntimeStatus() to detect the `jcode` CLI install (Hard Rules #15 + #17, #7263)
];
// Frozen pre-existing exceptions: spawn-capable routes NOT yet classified

View File

@@ -32,6 +32,8 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [
"/api/cli-tools/omp-settings", // spawns `which omp` to detect the CLI install (Hard Rules #15 + #17, #6318)
"/api/cli-tools/letta-settings", // spawns `which letta` to detect the CLI install (Hard Rules #15 + #17, #6318)
"/api/cli-tools/grok-build-settings", // GET calls getCliRuntimeStatus("grok-build"), which spawns a child process to locate + healthcheck the `grok` binary — same transitive-spawn surface that classified /api/skills/collect/ (Hard Rules #15 + #17). Writing ~/.grok/config.toml is inherently a local-machine operation, so loopback-only costs no real capability.
"/api/cli-tools/forge-settings", // spawns via getCliRuntimeStatus() to detect the `forge` CLI install (Hard Rules #15 + #17, #7263)
"/api/cli-tools/jcode-settings", // spawns via getCliRuntimeStatus() to detect the `jcode` CLI install (Hard Rules #15 + #17, #7263)
"/api/services/", // T-10: embedded service lifecycle (spawn child processes)
"/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

View File

@@ -0,0 +1,48 @@
/**
* Security regression: /api/cli-tools/forge-settings and /api/cli-tools/jcode-settings
* must be classified as LOCAL_ONLY so loopback enforcement runs unconditionally before
* any auth check.
*
* GET calls getCliRuntimeStatus(TOOL_ID) (src/app/api/cli-tools/forge-settings/route.ts,
* src/app/api/cli-tools/jcode-settings/route.ts), which spawns a child process to locate
* and healthcheck the CLI binary (src/shared/services/cliRuntime.ts:332). That is the same
* transitive-spawn surface that got /api/cli-tools/grok-build-settings and
* /api/skills/collect/ classified.
*
* Classifying it LOCAL_ONLY closes the remote-RCE vector: a leaked JWT over a
* Cloudflared/Ngrok tunnel cannot trigger process spawning.
* Hard Rules #15 + #17. See docs/security/ROUTE_GUARD_TIERS.md. Issue #7263.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { isLocalOnlyPath } from "../../src/server/authz/routeGuard.ts";
test("/api/cli-tools/forge-settings is LOCAL_ONLY (spawns via getCliRuntimeStatus)", () => {
assert.equal(isLocalOnlyPath("/api/cli-tools/forge-settings"), true);
});
test("/api/cli-tools/forge-settings with trailing slash is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/cli-tools/forge-settings/"), true);
});
test("/api/cli-tools/jcode-settings is LOCAL_ONLY (spawns via getCliRuntimeStatus)", () => {
assert.equal(isLocalOnlyPath("/api/cli-tools/jcode-settings"), true);
});
test("/api/cli-tools/jcode-settings with trailing slash is LOCAL_ONLY", () => {
assert.equal(isLocalOnlyPath("/api/cli-tools/jcode-settings/"), true);
});
test("sibling cli-tools spawn-capable settings routes stay LOCAL_ONLY", () => {
// Guards against a refactor dropping the established precedent this entry follows.
assert.equal(isLocalOnlyPath("/api/cli-tools/omp-settings"), true);
assert.equal(isLocalOnlyPath("/api/cli-tools/letta-settings"), true);
assert.equal(isLocalOnlyPath("/api/cli-tools/grok-build-settings"), true);
});
test("non-spawning cli-tools routes are NOT over-gated by this entry", () => {
// The new prefixes must not accidentally widen to the whole /api/cli-tools/ subtree,
// which remote dashboards legitimately use.
assert.equal(isLocalOnlyPath("/api/cli-tools/all-statuses"), false);
assert.equal(isLocalOnlyPath("/api/cli-tools/keys"), false);
});