Skip to content

fix(api): stop restricted-key /v1/models leaking aggregator rows via namespaced root - #15452

Merged
diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.52from
yourspraveen:fix/15409-allowlist-root-collision
Oct 6, 2026
Merged

diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.52from
yourspraveen:fix/15409-allowlist-root-collision

Conversation

@yourspraveen

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #15306

Closes #15409

Problem

A restricted API key's GET /v1/models listed models from providers the key never allowed. The catalog filter admitted a provider row when either its id or its raw root passed isModelAllowedForKey. root carries no owning-provider namespace, so an aggregator row such as cline's

id   = cline/deepseek/deepseek-v4-flash
root = deepseek/deepseek-v4-flash

was read as the deepseek provider's model. A key allowing only ds/deepseek-v4-flash therefore listed the cline row. The same happened with any provider wildcard: openrouter/* admitted cline's openrouter/free. Because the blocked-pattern check ran separately inside each of the two calls, blocking cline/* didn't help either: the root branch re-admitted the row.

Fix

The check moves into isCatalogModelAllowedForKey (src/app/api/v1/models/catalogKeyFilter.ts):

catalog.ts calls the helper in one line, which keeps the file under its frozen size baseline.

Relation to #15415

#15415 (@HouMinXi), opened today against the same issue, fixes a different over-match: in modelPermissions.ts it stops a provider/* wildcard such as cline/* from covering another provider nested under the same prefix. That is worth having, but it does not touch the path in this report. Here the key has no cline entry, and the cline row gets in through the catalog's isModelAllowedForKey(key, m.root) branch.

I checked this by running this PR's regression test on #15415's head (6a6b681775), with the catalog's original id || root check in place. R1 (deepseek-only key lists cline rows), R2 (openrouter/* admits cline's openrouter/free) and R4 (blocked id re-admitted through root) still fail there. The two PRs are complementary and touch different files, so they can merge in either order.

Tests

tests/unit/api-key-allowlist-root-namespace-15409.test.ts uses real DB keys and the real isModelAllowedForKey alias expansion:

  • R1: ds/deepseek-v4-flash key does not list cline/… or cl/…, and still lists deepseek/deepseek-v4-flash
  • R2: an openrouter/* key does not list cline's openrouter/free
  • R3: a bare gpt-4o entry still matches openai/gpt-4o ([Feature] Return only accessible models from /models for restricted API keys #781)
  • R4: a blocked cline/* row is not re-admitted through a bare root
  • R5: catalog.ts routes through the helper

R1, R2, R4 and R5 failed before the fix. All pass after it. Related suites: api-key-policy, apikeypolicy-disable-non-public, api-manager-provider-permissions, models-catalog-* and combo-restricted-key-target-policy-12886, 219/219 passing.

Local quality gates

I ran the quality.yml fast-gate set locally against release/v3.8.52, including typecheck:core, dashboard typecheck, the ts7 diagnostics ratchet, ESLint --max-warnings 0 on the changed files, and changelog integrity:

  • Passing (34): every gate this change could affect, including file-size, complexity-ratchets, dead-code, type-coverage, db-rules, error-helper, test-discovery, test-runner-api, mutation-test-coverage, ai-attribution, changelog-integrity, typecheck:core and dashboard-typecheck.
  • Failing (3), none caused by this change:
    • cycles and agent-skills-sync fail identically on the untouched base.
    • workflows needs actionlint and zizmor, which aren't installed locally; this PR doesn't touch workflows.

…namespaced root

The catalog filter admitted a provider row when either its id or its raw
`root` passed isModelAllowedForKey. `root` carries no owning-provider
namespace, so an aggregator row such as cline's
`cline/deepseek/deepseek-v4-flash` (root `deepseek/deepseek-v4-flash`) was
read as the deepseek provider's model and listed for a key that only
allowed `ds/deepseek-v4-flash`. The same applied to any provider wildcard
(`openrouter/*` admitted cline's `openrouter/free`).

The check moves to isCatalogModelAllowedForKey: the id decides; a bare root
still matches a bare allowlist entry (cheaper-inference#781), a namespaced root never does,
and a row whose id is blocked is not re-admitted through its root.

Closes cheaper-inference#15409
Keeps src/app/api/v1/models/catalog.ts under its frozen file-size baseline:
the call site shrinks to one line and the helper resolves its own
@/lib/db/apiKeys dependencies (tests still inject them).
@yourspraveen

Copy link
Copy Markdown
Contributor Author

CI note for reviewers:

The red checks on this PR are inherited from release/v3.8.52 (#15306) or from CI infrastructure. None of them involves the files this PR changes:

Check Cause Why it is not this PR
Fast Quality Gates only check:cycles fails (14 SCCs) fails identically on the untouched base; this PR adds no import edges in those cycles
Docs Sync (Strict) [i18n-check] drift detected i18n mirrors drifted on the base; this PR touches no translated file
Merge integrity check:agent-skills-sync regenerates omni-providers same dry-run diff on the base; no skill or provider-catalog change here
Lint stops at npm run audit:deps: 44 advisories (incl. next, node-forge) new advisories against the base lockfile; this PR changes no dependency
Build The runner has received a shutdown signal runner infrastructure, cancelled mid-job

The checks that exercise this change pass: API Route Typecheck, the unit-test shards and Vitest. Before pushing I ran the quality.yml fast-gate set locally (34 passing, including file-size, complexity, mutation-test-coverage, typecheck:core and dashboard typecheck), plus 219/219 related allowlist and catalog tests.

@diegosouzapw
diegosouzapw merged commit 6fc4375 into cheaper-inference:release/v3.8.52 Oct 6, 2026
44 of 51 checks passed
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.

fix(api): restricted API-key allowlist leaks models from other providers when a model root collides after provider-alias expansion

2 participants