Skip to content

fix(authz): gate the cli-tools status and skills execution routes to LOCAL_ONLY - #13745

Merged
diegosouzapw merged 1 commit into
release/v3.8.51from
fix/sec-local-only-gates-35fw-jx89
Sep 15, 2026
Merged

diegosouzapw merged 1 commit into
release/v3.8.51from
fix/sec-local-only-gates-35fw-jx89

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Fixes two privately reported advisories of the same class as /api/acp/agents (GHSA-hf57):
GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89. A route whose handler chain spawns a host
process sat on Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth under
requireLogin=false. Hard Rules #15/#17 require the LOCAL_ONLY gate, which runs on the stamped
real peer before any auth check.

GHSA-35fw — 14 cli-tools routes

All reach getCliRuntimeStatus() → locateCommand() → runProcess("sh", ["-c", 'command -v -- "$1"']) → spawn(),
exactly like the six siblings already gated (forge / grok-build / jcode / qwen / omp / letta):

  • all-statuses, status
  • claude / cline / codewhale / codex / crush / deepseek-tui / droid / kilo / openclaw / pi / smelt -settings
  • detect — missed by the advisory, and the heaviest: detectAllTools() runs execFile(binary, ["--version"]) and execFile("which") per tool.

The advisory's precondition is also narrower than stated: a fresh install is only open from
loopback, so the realistic exposure is an instance explicitly running with requireLogin=false.

GHSA-jx89 — skills install + executions

POST /api/skills/install stores handlerCode verbatim as the handler with no allowlist, so a
value equal to a built-in name (execute_command, eval_code) aliases the real sandboxed
built-in; POST /api/skills/executions then runs it. The sandbox is a genuine container
(--network none, --cap-drop ALL, read-only, allowlisted images), but the spawn is transitive,
so the 6A.8 route-guard source scan never saw it.

Change

  • 16 exact entries in LOCAL_ONLY_API_PREFIXES, mirrored in SPAWN_CAPABLE_PREFIXES (no manage-scope bypass).
  • The 16 route dirs added to check-route-guard-membership.ts roots, so the gate now enforces them.
  • No blanket /api/cli-tools/ prefix — apply, backups, config, guide-settings,
    hermes-agent-settings, keys, logs, openclaw/auto-order and codex-profiles do not spawn
    and remote dashboards use them.
  • ROUTE_GUARD_TIERS.md, x-loopback-only in openapi.yaml, changelog fragment.

Functional trade-off (same one already accepted for grok/forge/jcode/qwen): a dashboard served
through a tunnel no longer shows the CLI Tools status badges, and skill execution history is
loopback/LAN only.

Validation (TDD)

Check Result
New route-guard-cli-tools-settings-local-only + route-guard-skills-execute-local-only on the tip red
Route-guard, authz and membership suites after the fix 422 / 422
check:route-guard-membership OK — 82 routes in 24 roots, 0 gaps
check:openapi-security-tiers PASS (the traffic-inspector warnings are pre-existing)
typecheck:core, check:docs-sync, prettier clean
eslint clean under the repo's suppressions file

Two existing negative controls pointed at routes that turn out to spawn
(/api/cli-tools/all-statuses, /api/skills/install). They were replaced, not removed, with
routes that genuinely do not spawn (/api/cli-tools/config, /api/skills/marketplace,
/api/skills/skillssh/install), so the non-over-gating assertions still hold.

⚠️ base-red inherited: #12732 — failures outside the route-guard suites come from the tip.

…LOCAL_ONLY

GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89 — the same defect class as
/api/acp/agents (GHSA-hf57): a route whose handler chain spawns a host process
was classified Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth
when requireLogin=false. Hard Rules #15/#17 require the LOCAL_ONLY gate, which
runs on the stamped real peer before any auth check.

cli-tools (GHSA-35fw): 14 routes reach
getCliRuntimeStatus() -> locateCommand() -> runProcess("sh", ["-c",
'command -v -- "$1"']) -> spawn(), exactly like their six gated siblings
(forge/grok-build/jcode/qwen/omp/letta-settings):
all-statuses, status, and the claude/cline/codewhale/codex/crush/deepseek-tui/
droid/kilo/openclaw/pi/smelt-settings routes. The advisory counted 13; it
missed /api/cli-tools/detect, which is heavier — detectAllTools() runs
execFile(binary, ["--version"]) and execFile("which") per tool.

skills (GHSA-jx89): POST /api/skills/install stores the request's handlerCode
verbatim as the skill handler with no allowlist, so a value equal to a built-in
name (execute_command / eval_code) aliases the real sandboxed built-in;
POST /api/skills/executions then runs it. The sandbox is a real container, but
the spawn is transitive, which is why the 6A.8 source scan never flagged it.

Entries are exact paths, not a /api/cli-tools/ blanket prefix: apply, backups,
config, guide-settings, hermes-agent-settings, keys, logs, openclaw/auto-order
and codex-profiles do not spawn and remote dashboards use them. All 16 are
mirrored into SPAWN_CAPABLE_PREFIXES (no manage-scope bypass) and added to the
route-guard-membership roots so the gate enforces them from now on.

Functional trade-off, same one already accepted for grok/forge/jcode/qwen: a
dashboard served through a tunnel no longer shows the CLI Tools status badges.

Tests are red-first. Two existing negative controls pointed at routes that turn
out to spawn (/api/cli-tools/all-statuses, /api/skills/install); they now point
at routes that genuinely do not (/api/cli-tools/config, /api/skills/marketplace,
/api/skills/skillssh/install), so the non-over-gating assertions are kept.
@diegosouzapw
diegosouzapw merged commit c8b24ff into release/v3.8.51 Sep 15, 2026
16 of 21 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…LOCAL_ONLY (diegosouzapw#13745)

GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89 — the same defect class as
/api/acp/agents (GHSA-hf57): a route whose handler chain spawns a host process
was classified Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth
when requireLogin=false. Hard Rules diegosouzapw#15/diegosouzapw#17 require the LOCAL_ONLY gate, which
runs on the stamped real peer before any auth check.

cli-tools (GHSA-35fw): 14 routes reach
getCliRuntimeStatus() -> locateCommand() -> runProcess("sh", ["-c",
'command -v -- "$1"']) -> spawn(), exactly like their six gated siblings
(forge/grok-build/jcode/qwen/omp/letta-settings):
all-statuses, status, and the claude/cline/codewhale/codex/crush/deepseek-tui/
droid/kilo/openclaw/pi/smelt-settings routes. The advisory counted 13; it
missed /api/cli-tools/detect, which is heavier — detectAllTools() runs
execFile(binary, ["--version"]) and execFile("which") per tool.

skills (GHSA-jx89): POST /api/skills/install stores the request's handlerCode
verbatim as the skill handler with no allowlist, so a value equal to a built-in
name (execute_command / eval_code) aliases the real sandboxed built-in;
POST /api/skills/executions then runs it. The sandbox is a real container, but
the spawn is transitive, which is why the 6A.8 source scan never flagged it.

Entries are exact paths, not a /api/cli-tools/ blanket prefix: apply, backups,
config, guide-settings, hermes-agent-settings, keys, logs, openclaw/auto-order
and codex-profiles do not spawn and remote dashboards use them. All 16 are
mirrored into SPAWN_CAPABLE_PREFIXES (no manage-scope bypass) and added to the
route-guard-membership roots so the gate enforces them from now on.

Functional trade-off, same one already accepted for grok/forge/jcode/qwen: a
dashboard served through a tunnel no longer shows the CLI Tools status badges.

Tests are red-first. Two existing negative controls pointed at routes that turn
out to spawn (/api/cli-tools/all-statuses, /api/skills/install); they now point
at routes that genuinely do not (/api/cli-tools/config, /api/skills/marketplace,
/api/skills/skillssh/install), so the non-over-gating assertions are kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant