Skip to content

fix(proxy): treat omitted auth on config pass-through routes as enforced at registration - #44253

Closed
devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
litellm_fix_816dc1
Closed

devin-ai-integration[bot] wants to merge 3 commits into
mainfrom
litellm_fix_816dc1

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

How it solves it:

  • Registration now reads endpoint_data.get("auth", True), matching request-time auth
  • Exact path and include_subpath wildcard land in openai_routes again

User Flow

Before: an admin's config pass-through route with no auth key hides upstream failures from spend logs

  1. The admin adds /audit-pt to general_settings.pass_through_endpoints with include_subpath: true and no auth
  2. A developer sends POST https://litellm-domain/audit-pt with a valid virtual key
  3. The upstream rejects it and the developer gets back a 403 with the upstream error body
  4. The admin queries GET https://litellm-domain/spend/logs and finds no row for that request

After: the same failed request shows up in spend logs as a failure row

  1. Same config, no auth key
  2. Same POST https://litellm-domain/audit-pt with a valid virtual key
  3. Same 403 with the upstream error body
  4. GET https://litellm-domain/spend/logs now returns a failure row for that request id with error code 403

Relevant issues

Regression from #43962

Affected release

Regression since v1.105.0-dev.2 (commit 2eb2bf1, also on rc/1.105.0). Cherry-pick targets: stable/1.103.x, rc/1.104.0, rc/1.105.0

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

DB-backed proxy. Measured on a live proxy (2 workers, real Postgres, scripted upstream that always answers 403 {"error": {"message": "max budget reached for this deployment"}}) at each revision with the same config: /audit-pt with include_subpath: true and no auth, /audit-pt-off with auth: false, plus DB endpoints created through POST /config/pass_through_endpoint (/audit-db default auth, /audit-db-off with auth: false). Keys: master key, a non-admin internal-user key with no allowed_passthrough_routes, and a key on a blocked team (to see whether common_checks runs)

Check 2eb2bf1^ main (ac59ec6) fix (58fa9b2)
Failure spend row, valid key, POST /audit-pt, upstream 403 written missing written
/audit-pt in openai_routes yes no yes
/audit-pt/* in openai_routes yes no yes
is_auth_enforced_pass_through_route exact / subpath true / true false / false true / true
No key, /audit-pt and /audit-pt/sub 401 / 401 401 / 401 401 / 401
Non-admin key without allowed_passthrough_routes, /audit-pt forwarded (upstream 403), failure row forwarded (upstream 403), no row forwarded (upstream 403), failure row
Same key, /audit-pt/sub 403 allowed_passthrough_routes 401 "Only proxy admin" 403 allowed_passthrough_routes
common_checks on /audit-pt (blocked-team key) skipped, forwarded skipped, forwarded skipped, forwarded
common_checks on /audit-pt/sub runs (403 route gate) runs (401 admin-only route) runs (403 route gate)
auth: false /audit-pt-off: no key, openai_routes, spend row forwarded, no, none forwarded, no, none forwarded, no, none
DB /audit-db: openai_routes exact/wildcard, no key, valid key not served in this rig (404) yes/yes, 401, 403 allowed_passthrough_routes yes/yes, 401, 403 allowed_passthrough_routes
DB /audit-db-off (auth: false): openai_routes, no key not served in this rig (404) no, forwarded no, forwarded
Key with allowed_routes: ["llm_api_routes"], /audit-pt 403 allowed_passthrough_routes forwarded (upstream 403) 403 allowed_passthrough_routes
Over-budget key (max_budget: 0), /audit-pt and /audit-pt/sub 422 / 422 budget_exceeded forwarded / 401 "Only proxy admin" 422 / 422 budget_exceeded

DB-less proxy (no database_url). Without a prisma client the parent never re-registered config entries through PassThroughGenericEndpoint, so omitted auth stayed unenforced there and this PR does not match 2eb2bf1^ on DB-less proxies. Measured with one worker, the same scripted upstream, /audit-pt with include_subpath: true and no auth, enforce_user_param: true and reject_clientside_metadata_tags: true, master key on every request

Check (DB-less) 2eb2bf1^ main (688d791) fix (1de04ce)
/audit-pt and /audit-pt/* in openai_routes no / no no / no yes / yes
POST /audit-pt/sub without user forwarded (upstream 403) forwarded (upstream 403) 401 'user' param not passed in
POST /audit-pt/sub with metadata.tags forwarded (upstream 403) forwarded (upstream 403) 400 Client-side 'metadata.tags' not allowed
Same two requests on the exact path /audit-pt forwarded / forwarded forwarded / forwarded forwarded / forwarded

The common_checks skip on the exact path is the pre-existing endpoint.get("auth") is not True check in user_api_key_auth.py, unchanged here because #39017 and #43250 own that policy

Shared setup for the curl proof below:

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY
  database_url: os.environ/DATABASE_URL
  proxy_batch_write_at: 1
  pass_through_endpoints:
    - path: /audit-pt
      target: http://127.0.0.1:8291/upstream
      include_subpath: true

KEY is a fresh virtual key from POST /key/generate with key_alias: audit-proof, MK is the master key

Before (ac59ec6)

  1. Spend logs before the request
$ curl -s 'localhost:4420/spend/logs?start_date=2026-01-01&end_date=2027-01-01&summarize=false' -H "Authorization: Bearer $MK" | jq '[.[] | select(.call_type=="pass_through_endpoint")] | length'
0
  1. Request with a valid key, upstream answers 403
$ curl -s -i -X POST localhost:4420/audit-pt -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"contents":[{"parts":[{"text":"hi"}]}]}' | grep -iE '^HTTP|^x-litellm-call-id|^\{'
HTTP/1.1 403 Forbidden
x-litellm-call-id: 1dc73808-be44-4b47-99ee-dbe1a1c12f2e
{"error": {"message": "max budget reached for this deployment"}}
  1. Spend logs after polling for 60s: still no row
$ curl -s 'localhost:4420/spend/logs?start_date=2026-01-01&end_date=2027-01-01&summarize=false' -H "Authorization: Bearer $MK" | jq '[.[] | select(.call_type=="pass_through_endpoint") | {request_id, status, call_type, api_key_alias: .metadata.user_api_key_alias, error_class: .metadata.error_information.error_class, error_code: .metadata.error_information.error_code, normalized_error: .metadata.error_information.normalized_error}]'
[]

After (58fa9b2)

  1. Spend logs before the request
$ curl -s 'localhost:4430/spend/logs?start_date=2026-01-01&end_date=2027-01-01&summarize=false' -H "Authorization: Bearer $MK" | jq '[.[] | select(.call_type=="pass_through_endpoint")] | length'
0
  1. Request with a valid key, upstream answers 403
$ curl -s -i -X POST localhost:4430/audit-pt -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"contents":[{"parts":[{"text":"hi"}]}]}' | grep -iE '^HTTP|^x-litellm-call-id|^\{'
HTTP/1.1 403 Forbidden
x-litellm-call-id: c4f43c72-9bec-4899-b88b-f7ba2e6905ff
{"error": {"message": "max budget reached for this deployment"}}
  1. Spend logs after: the failure row for that call id
$ curl -s 'localhost:4430/spend/logs?start_date=2026-01-01&end_date=2027-01-01&summarize=false' -H "Authorization: Bearer $MK" | jq '[.[] | select(.call_type=="pass_through_endpoint") | {request_id, status, call_type, api_key_alias: .metadata.user_api_key_alias, error_class: .metadata.error_information.error_class, error_code: .metadata.error_information.error_code, normalized_error: .metadata.error_information.normalized_error}]'
[
  {
    "request_id": "c4f43c72-9bec-4899-b88b-f7ba2e6905ff",
    "status": "failure",
    "call_type": "pass_through_endpoint",
    "api_key_alias": "audit-proof",
    "error_class": "HTTPException",
    "error_code": "403",
    "normalized_error": "500_UPSTREAM_PASSTHROUGH"
  }
]

Admin UI Logs page (http://localhost:/ui/?page=logs, logged in as admin) right after the same request: main shows "No requests yet", fix shows the Failure row for c4f43c72 with key alias audit-proof

The head after review, 1de04ce, only wraps long test lines and rewords one suppression reason in the unit test, so the DB-backed rows measured on 58fa9b2 carry over. Terminal audit on head 58fa9b2: the same integration node, run twice back to back through the integration rig against the PR head, passed both times (1 passed in 28.77s, 1 passed in 25.74s). Post-review live risk: no code changed after the bot loop (Greptile 5/5 and Bugbot clean on 58fa9b2), so the A/B matrix above still describes the head

The existing integration node tests/integration/observability/test_passthrough_upstream_error_visibility.py::test_config_pass_through_route_logs_body_and_strips_query is unchanged and passes at 2eb2bf1^, fails on main (State did not converge: [] waiting for the spend row) and passes at this PR's head

Type

🐛 Bug Fix
✅ Test

Caveats (if any)

Severe

  • Config routes without auth regain parent behavior, so non-admin keys need allowed_passthrough_routes on subpaths again
    • Same as 2eb2bf1^ on DB-backed proxies, see the first table
  • DB-less proxies newly enforce auth on config routes without auth, unlike 2eb2bf1^
    • Under enforce_user_param: true, a master-key POST to a subpath without user now gets 401
    • Under reject_clientside_metadata_tags: true, client metadata.tags on a subpath now gets 400
    • JWT team callers without a passthrough grant are now gated by allowed_passthrough_routes
    • Operators who want the old DB-less behavior set auth: false on the entry
  • Keys limited to allowed_routes: ["llm_api_routes"] now get 403 on the exact path too, and over-budget keys get 422 on both paths
    • Both match 2eb2bf1^ on DB-backed proxies

Low

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/90bf11d0f3a449eda38ad50de59af074
Open in Devin Desktop: https://app.devin.ai/desktop/session/90bf11d0f3a449eda38ad50de59af074?variant=devin


Note

Medium Risk
Changes proxy auth registration for config pass-throughs (including DB-less deployments) and restores stricter key/route checks; operators who relied on omitted auth must set auth: false explicitly.

Overview
Config pass-through routes with no auth key are registered as authenticated again, matching request-time behavior in user_api_key_auth (get("auth", True)).

Registration now uses endpoint_data.get("auth", True) instead of bare get("auth"), so omitted auth enables user_api_key_auth on the route, adds exact and wildcard paths to LiteLLMRoutes.openai_routes, and restores spend/failure logging and route gates that regressed when omitted auth was treated as unenforced.

Explicit auth: false is unchanged. New parametrized unit tests cover config dicts and DB PassThroughGenericEndpoint registration for exact and subpaths.

Reviewed by Cursor Bugbot for commit 1de04ce. Bugbot is set up for automated code reviews on this repo. Configure here.

@devin-ai-integration
devin-ai-integration Bot requested a review from a team October 2, 2026 21:38
@devin-ai-integration

devin-ai-integration Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical risk] Changes authentication enforcement default for proxy pass-through routes.

The PR appears safe to merge; the latest changes preserve the test behavior

Summary

Config pass-through routes now treat omitted auth as True during registration. Tests cover exact paths, subpaths, database defaults, and explicit auth: false

  • Changes since the previous review only rework a test helper and wrap test arguments. No new actionable issues were found
  • devin-ai-integration[bot] acknowledges stricter checks on database-less proxies as intentional and documents auth: false as the opt-out
  • devin-ai-integration[bot] explicitly defers the existing exact-path common-check skip to separate work

Reviews (2) · Last reviewed commit: "test(proxy): wrap long pass-through auth..."

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

@codspeed

codspeed Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_816dc1 (1de04ce) with main (688d791)

Open in CodSpeed

@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1de04ce. Configure here.

@yuneng-berri

Copy link
Copy Markdown
Contributor

@veria-ai run

@yuneng-berri

Copy link
Copy Markdown
Contributor

Closing: #43962 correctly restored pre-config-wins auth. The red test relied on a config wins side effect, fixed test-side in #44265.

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