Skip to content

fix(security): enforce allowedEndpoints on the alias rewrites (#13685) - #13741

Merged
diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.51from
gonisulaimann:fix/endpoint-alias-fail-open-13685
Sep 18, 2026
Merged

diegosouzapw merged 1 commit into
cheaper-inference:release/v3.8.51from
gonisulaimann:fix/endpoint-alias-fail-open-13685

Conversation

@gonisulaimann

Copy link
Copy Markdown
Contributor

Fixes #13685.

#13684 closed the endpoint-allowlist hole on the direct /api/v1/... spelling. The alias rewrites in next.config.mjs were the other half of it. A route handler sees the client's original URL, so /chat/completions, /responses, /responses/*, /models, /codex/* and anything under /v1/v1/* arrived at validateEndpointAccess with a pathname resolveEndpointCategory() doesn't recognize. It returns null, validateEndpointAccess reads that as "no category, nothing to check", and the request goes through — so a key created with allowedEndpoints: ["search"] could still reach chat through /chat/completions or /codex/*, and any endpoint at all through /v1/v1/*.

The fix is the alias map from the issue. resolveCanonicalEndpointPath() sits next to resolveEndpointCategory() and folds the client-facing spellings onto the /v1/... form the category table speaks before the lookup runs; validateEndpointAccess now calls it in place of the /api-only strip #13684 added. The table mirrors the rewrite list, first match wins. /codex/:path* folds its sub-path onto /api/v1/responses, and the mapped form keeps the sub-path rather than collapsing it — it still resolves to chat, since /v1/responses is the matching prefix, so there is no codex special case and none is needed.

I couldn't use the getRequestMeta(req, "rewrittenPathname") alternative you mentioned — there's no such helper and no rewritten-path header anywhere in the tree, so the alias map is the only route available. resolveEndpointCategory has exactly one consumer, so the normalization lands at the single place that matters.

Two things from the issue I left out on purpose, so tell me if you want them folded in:

  • The catch {} around the URL parse stays fail-open as it is. request.url is always absolute inside an App Router handler, so the branch is effectively unreachable, and making it fail closed is a behaviour change of its own rather than part of the alias fix.
  • The bare x-api-key without anthropic-version is the route-level extractApiKey path, a different layer from this policy check.

Tests: tests/unit/endpoint-categories.test.ts covers the pure canonicalizer — every alias, the sub-path forms, both foldings, and that already-canonical or uncategorised paths come back untouched. tests/unit/endpoint-restrictions-policy.test.ts adds one policy assertion per alias spelling the issue lists (chat, responses, responses sub-path, codex, /v1/v1, models) plus a control that a key allowed on search still reaches /v1/search. The six alias tests fail with the canonicalizer reverted; the control and the existing #13684 tests pass either way.

Verified locally: 56/56 across the two suites, 106/106 across the eleven suites importing apiKeyPolicy or endpointCategories, 17/17 in tests/integration/api-keys.test.ts. typecheck:core, npm run lint, prettier, scripts/check/check-api-typecheck.mjs, check:open-sse-typecheck, the complexity and cognitive-complexity ratchets, check:duplication and check:changelog-integrity are all green. check:file-size is green in base-relative mode; run without --base-ref it reports open-sse/utils/stream.ts at 3115 against its frozen 3098, which comes from the base and is untouched here.

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Thanks @gonisulaimann — merging via the release merge-train. Validated in local merge-train (.claude/worktrees/merge-train-20260918-111718-suite.log) on the devbox @ train tip 7bb373fba5e241964c0ffb17bd03a804700839bb, boarded with 55 sibling PRs: typecheck:core, file-size, complexity, cognitive-complexity, changelog-integrity green; 747/747 changed-area node:test cases + 476/476 vitest green (fast parity mode — the full suite ran today on the release tip via the base-red train and runs again on the 3b train). Merged --admin per merge-gates §7.

@diegosouzapw
diegosouzapw merged commit 21d0e81 into cheaper-inference:release/v3.8.51 Sep 18, 2026
11 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…r-inference#13685) (cheaper-inference#13741)

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
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.

security(policy): allowedEndpoints still fails open on the non-/v1 alias rewrites (/chat/completions, /responses, /codex/*, /v1/v1/*)

2 participants