Skip to content

fix(sse): keep a model name from rewriting the upstream URL path - #15067

Merged
diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
HouMinXi:fix/model-name-path-injection
Sep 29, 2026
Merged

diegosouzapw merged 2 commits into
cheaper-inference:release/v3.8.51from
HouMinXi:fix/model-name-path-injection

Conversation

@HouMinXi

@HouMinXi HouMinXi commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #15032

Summary

Many providers put the model id into the upstream URL path (.../models/{id}:generateContent, .../deployments/{id}/...). The model name comes from the request, and parseModel only rejected a literal ../ or a backslash-dot pair. The URL parser that fetch uses folds %2e%2e into a dot-segment, ends the path at # and starts a query at ?, so gemini/%2e%2e/cachedContents# made the executor POST to /v1beta/cachedContents and gemini/.%2e/.%2e/upload/v1beta/files?x= to /upload/v1beta/files, with the operator's key attached. The same string reaches Vertex's project-scoped paths with the service-account token. It also defeats a per-key model allow-list: a key limited to gemini/gemini-2.5-flash* matched gemini/gemini-2.5-flash/%2e%2e/gemini-3-pro, which resolves to models/gemini-3-pro. A catalog that is not marked authoritative for the provider does not stop it, since an unknown id is only refused when the catalog is.

Refuse, in parseModel and in every registry parser (image, embedding, rerank, moderation, OCR, video, music, upscale and the three audio ones), an id that has a query or fragment delimiter, a backslash, a percent-encoded dot, slash, backslash, #, ? or NUL, or a . / .. path segment. Slashes, colons, @, brackets and dots inside names are unchanged; none of the 3,673 model ids in the built-in catalogs is affected.

Related Issues

  • None.

Validation

  • Change type: other (authz / security hardening)
  • Focused tests and category gates from the golden path: the test listed below, the 144 existing test files that use the model parsers (1,170 tests), npm run typecheck:core
  • npm run lint on the changed files (Prettier and ESLint clean)
  • Reconciled with the current active release base (release/v3.8.51 at b27c254430); focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR

Each new test was run against the change reverted (it fails) and restored (it passes).

Tests Added Or Updated

  • tests/unit/model-id-path-injection.test.ts

Coverage Notes

The listed test exercises every changed production path: the helper on unsafe and ordinary ids, parseModel, all eleven registry parsers, and the Gemini upstream URL built from the parse result.

Reviewer Notes

Five tests in that run fail on the current tip with and without this change (tests/unit/models-catalog-low-noise-flag.test.ts x3, tests/unit/suffix-effort-propagation.test.ts x2). Voice ids already have their own validators; this closes the same class for model ids. The Codex /v1/responses/* sub-path can still carry an encoded slash (..%2f) to the upstream literally; dot-segments there are already removed by URL normalisation, so I left it.

Maintainer rework (merge-batch 2026-09-28 (release drain))

  • The new test used new (DefaultExecutor as any)(...); no-explicit-any is an error under tests/, so the stand-in is typed now. No production change.
  • Checked the 2,791 ids in PROVIDER_MODELS against hasUnsafeModelIdSyntax: none is flagged.
  • Red/green: with the production files reverted to the tip the new test fails 3/6; with the PR it passes 6/6.

Many providers put the model id into the upstream URL path
(`.../models/{id}:generateContent`, `.../deployments/{id}/...`). The model
name comes from the request, and parseModel only rejected a literal `../`
or a backslash-dot pair. The URL parser that fetch uses folds `%2e%2e` into
a dot-segment, ends the path at `#` and starts a query at `?`, so
`gemini/%2e%2e/cachedContents#` made the executor POST to
`/v1beta/cachedContents` and `gemini/.%2e/.%2e/upload/v1beta/files?x=` to
`/upload/v1beta/files`, with the operator's key attached. The same string
reaches Vertex's project-scoped paths with the service-account token.
It also defeats a per-key model allow-list: a key limited to
`gemini/gemini-2.5-flash*` matched
`gemini/gemini-2.5-flash/%2e%2e/gemini-3-pro`, which resolves to
`models/gemini-3-pro`. A catalog that is not marked authoritative for the
provider does not stop it, since an unknown id is only refused when the
catalog is.

Refuse, in parseModel and in every registry parser (image, embedding,
rerank, moderation, OCR, video, music, upscale and the three audio ones),
an id that has a query or fragment delimiter, a backslash, a percent-encoded
dot, slash, backslash, `#`, `?` or NUL, or a `.` / `..` path segment. Slashes,
colons, `@`, brackets and dots inside names are unchanged; none of the 3,673
model ids in the built-in catalogs is affected.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
no-explicit-any is an error under tests/, so the new model-id test failed lint.
@diegosouzapw
diegosouzapw merged commit 2611af3 into cheaper-inference:release/v3.8.51 Sep 29, 2026
11 of 16 checks passed
diegosouzapw added a commit that referenced this pull request Sep 29, 2026
… 4) (#15109)

Release-captain base-red fix (v3.8.51 release PR #11442, unit shards 3-4): one production defect (proxyLogger pulled into every settings→proxies import and queried the DB at import time; helper extracted to src/lib/proxyLogHost.ts) and seven contract propagations from #14732, #15044, #15067, #13548, #14117×#14844, #12810. 74/74 across the seven files, 176/176 proxy-log neighbours, typecheck clean.
@HouMinXi
HouMinXi deleted the fix/model-name-path-injection branch October 28, 2026 10:12
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.

2 participants