Repository navigation
fix(authz): loopback-gate the two provider routes that spawn child processes (#15159 S-01) - #15242
Merged
diegosouzapw merged 2 commits intoOct 2, 2026
Conversation
…ocesses (cheaper-inference#15159 S-01) Hard Rules #15 + #17 were broken on two /api/providers/ surfaces: the spawn runs before any auth check, so a leaked JWT via tunnel could reach it. Hop 1 - fixed paths, spawn is unconditional (webProvidersB.ts:535, fixed binary + fixed argv). Classified LOCAL_ONLY via a new pattern in LOCAL_ONLY_API_PATTERNS, scoped to the three exact paths so the rest of /api/providers/ CRUD stays remote-reachable. Hop 2 - /api/providers/{id}/models reaches cursorAgent.ts:17, but only on the provider === "cursor" branch, and {id} is a CONNECTION id rather than a provider name. No path pattern can separate it from the ~50 other providers' pure-HTTP discovery in the same route, so a [^/]+/models pattern would lock remote model discovery for every non-Cursor provider - the exact over-broadening the /login precedent exists to prevent. The spawn is instead gated at its call site on x-omniroute-peer-locality === "loopback" (stamped by the authz pipeline from the real TCP peer, never the spoofable Host header), failing closed to the cached/local catalog - the same pattern as handleCursorAgentImageGeneration. The membership gate cannot follow this transitive, provider-conditional chain (that is the G-09 blind spot, tracked separately), so the route is frozen in KNOWN_UNCLASSIFIED_SOURCE_SPAWN with a justification pointing at the guard, as the audit prescribes for known call chains. The freeze-empty assertion is replaced by two equivalent guards: the resolved routes may never be re-frozen, and every entry must carry a substantive justification. TDD: wrote the failing membership tests first and watched all three fail (validate/import/bulk were reachable), then applied the fix. - tests/unit/authz/route-guard-providers-spawn-local-only.test.ts (new) - tests/unit/check-route-guard-membership.test.ts (freeze invariants) - docs/openapi.yaml: x-loopback-only on the three newly gated paths
…local catalog
The non-loopback guard in /api/providers/{id}/models fell through to
fetchCursorAgentModels() (a child-process spawn) whenever
buildDiscoveryFallbackResponse() returned null. Return an explicit 403 instead and cover
the empty cache + empty catalog path with a behavioral route test.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
diegosouzapw
merged commit Oct 2, 2026
358d200
into
cheaper-inference:release/v3.8.52
8 of 9 checks passed
This was referenced Oct 9, 2026
woodsonl
added a commit
to woodsonl/OmniRoute
that referenced
this pull request
Oct 11, 2026
…oute tier A reverse-proxied dashboard (docker + nginx) got 403 LOCAL_ONLY on /api/providers/validate for every provider key — rendered by the Add API Key modal as a bare 'Invalid' badge with no reason, because the modal only rendered string-shaped error bodies. cheaper-inference#15242 added the blanket pattern for the devin CLI-spawn fallback; cheaper-inference#15766 already established the precise fix (peer-locality gate at the spawn call site) but only wired the test/create/test-batch routes. Drop the blunt pattern, thread allowLocalSpawn into validate/bulk/import (cheaper-inference#15766 shape), and surface {code,message} error envelopes in the modals via validationErrorMessage().
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes S-01 from the v3.8.52 whole-repository audit (#15159) — the next item in the owner's merge order.
Two
/api/providers/surfaces spawn child processes with no loopback gate, breaking Hard Rules #15 / #17: the spawn runs before any auth check, so a leaked JWT via a tunnel could reach it.Hop 1 — fixed paths, spawn is unconditional
src/lib/providers/validation/webProvidersB.ts:535—spawn(bin, ["acp", "--agent-type", "summarizer"], …), reached fromPOST /api/providers/{validate,import,bulk}. The binary comes from a fixed candidate list and argv is fixed, so this is not caller-controlled RCE — but the loopback gate is still mandatory.Classified via a new
LOCAL_ONLY_API_PATTERNSentry scoped to exactly those three paths, so ordinary provider CRUD stays remote-reachable.Hop 2 — deliberately NOT path-classified
src/app/api/providers/[id]/modelstransitively spawns viafetchCursorAgentModels() → runCursorAgent() → spawn()(src/lib/providerModels/cursorAgent.ts:17) — but only on theprovider === "cursor"branch, and{id}is a connection id, never a provider name.A
[^/]+/modelspattern therefore cannot separate that one spawn from the ~50 other providers' pure-HTTP discovery living in the same route; it would lock remote model discovery for every non-Cursor provider — precisely the over-broadening the existing/api/providers/[^/]+/loginprecedent exists to prevent.Enforced instead at the call site: the route requires
x-omniroute-peer-locality === "loopback"(stamped by the authz pipeline from the real TCP peer, never the spoofableHostheader) before callingfetchCursorAgentModels(), failing closed to the cached/local catalog otherwise — the same pattern ashandleCursorAgentImageGenerationincursorAgentImage.ts.Gate bookkeeping
The membership gate matches
spawn(/exec(only insideroute.ts, so it cannot follow this transitive, provider-conditional chain (that is the G-09 blind spot, tracked separately). The route is frozen inKNOWN_UNCLASSIFIED_SOURCE_SPAWNwith a justification pointing at the guard — the remedy the audit itself prescribes for G-09 ("freeze the known call chains").The old
KNOWN_UNCLASSIFIED_SOURCE_SPAWN must be emptyassertion is replaced by two equivalent guards rather than simply deleted: the two already-resolved routes may never be re-frozen, and every entry must carry a substantive justification.docs/openapi.yaml:x-loopback-only: trueadded to the three newly gated paths.Verification
tests/unit/authz/,tests/unit/route-guard-*.test.ts,security-route-guard-tiers,check-route-guard-membership.npm run check:route-guard-membership→ OK, 0 new gaps.npm run typecheck:core→ clean.npm run check:docs-all→ all gates pass.Pre-existing issues observed (not caused by this change, not fixed here)
check-openapi-security-tiers.mjsalready fails on CRLF checkouts: its//.*$line-comment strip cannot match past\r, so any pattern with a trailing comment is silently dropped from the parsed set (this drops the existingvolcengine-plan/connectpattern → 6 spurious mismatches). Reproduced against the unmodified base file. The new pattern puts its justification on its own line so it parses under both LF and CRLF. Worth folding into the "lying gates" batch.tests/unit/cursor-agent-models.test.ts— 2 sigkill tests fail on Windows withspawn … ENOENT(fake shebang binary is not directly executable on Windows). No cursor files are touched by this diff.Notes
jonlwheat2-gif/OmniRoute:release/v3.8.52(basedbe703a000).