Skip to content

refactor(proxy/auth): cherry-pick #29343 into patch/v1.87.0rc1 - #29362

Merged
yuneng-berri merged 1 commit into
patch/v1.87.0rc1from
patch/v1.87.0rc1-cp-29343
May 31, 2026
Merged

refactor(proxy/auth): cherry-pick #29343 into patch/v1.87.0rc1#29362
yuneng-berri merged 1 commit into
patch/v1.87.0rc1from
patch/v1.87.0rc1-cp-29343

Conversation

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Relevant issues

Cherry-pick of #29343 (merge commit 94a043efb2) onto the patch/v1.87.0rc1 branch.

Linear ticket

n/a

Pre-Submission checklist

CI (LiteLLM team)

  • Branch creation CI run -- link:
  • CI run for the last commit -- link:
  • Merge / cherry-pick CI run -- links:

Bug verification on v1.87.0-rc.1

Confirmed the bug is present at the v1.87.0-rc.1 tag (head of patch/v1.87.0rc1) before this cherry-pick lands. _safe_hash_litellm_api_key only branches on sk- prefix and JWT; any other input falls through to return api_key unchanged, so passing the raw Authorization header value (Bearer sk-...) returns the literal string.

$ git show v1.87.0-rc.1:litellm/proxy/_types.py | sed -n '2737,2750p'
    def _safe_hash_litellm_api_key(cls, api_key: str) -> str:
        """
        Helper to ensure all logged keys are hashed
        Covers:
        1. Regular API keys from LiteLLM DB
        2. JWT tokens used for connecting to LiteLLM API
        """
        if api_key.startswith("sk-"):
            return hash_token(api_key)
        from litellm.proxy.auth.handle_jwt import JWTHandler

        if JWTHandler.is_jwt(token=api_key):
            return f"hashed-jwt-{hash_token(token=api_key)}"
        return api_key

Effect on the v1.87.0-rc.1 build: any code path that hands the raw header value to this helper (e.g. observability labels such as Prometheus litellm_proxy_failed_requests_metric_total{hashed_api_key=...}) ends up emitting the unhashed Bearer sk-... string as a metric label, leaking the key.

Screenshots / Proof of Fix

End-to-end proof-of-fix (curl-driven Prometheus scrape showing the same metric row going from hashed_api_key="Bearer sk-..." to a proper sha256 hash) is captured in the original PR body at #29343 and was run against a live proxy with prometheus enabled. The cherry-pick is verbatim, so the same harness output applies.

Type

Refactoring + Test

Changes

Verbatim cherry-pick of #29343's merge commit 94a043efb226c5ccdbfc028fbb930ce45fb965eb onto patch/v1.87.0rc1. Auto-merged cleanly (no conflicts). 3 files, +30/-7 -- matches the sum of the three original commits exactly (+8/-5 in _types.py, +4/-2 in the MCP auth test, +18/-0 for the new contract test). Full rationale lives in the #29343 PR body.

…9343)

* refactor(proxy/auth): normalize Bearer prefix in safe-hash helper

UserAPIKeyAuth._safe_hash_litellm_api_key now strips a leading
"Bearer "/"bearer " prefix before its existing sk-/JWT classification, so
the helper produces the same hashed output regardless of whether the
caller stripped the Authorization header prefix or passed the header
value through unchanged.

* refactor(proxy/auth): make Bearer-prefix strip case-insensitive

Per RFC 7235 the HTTP authorization scheme token is case-insensitive.
Replace the two-prefix loop with a single case-insensitive check so the
helper normalizes "Bearer ", "bearer ", "BEARER ", and any mixed-case
variant before classifying the remainder as sk- or JWT. The contract
test gains coverage of "BEARER " and "BeArEr ".

* test(mcp): align auth-handler test expectations with safe-hash helper

The two MCP auth tests asserted that UserAPIKeyAuth(api_key="Bearer ...")
retained the raw header bytes on the api_key field. _safe_hash_litellm_api_key
now normalizes that input — stripping the Bearer prefix and hashing the
resulting sk- key — so the expectations move to the normalized form:
the bare token in the parametrize case, and hash_token("sk-...") in the
backward-compat assertion. This matches what the real auth flow produces
(the builder strips Bearer and the DB stores the hashed token), so the
mocks now line up with production rather than with the un-normalized
validator output.
@greptile-apps

greptile-apps Bot commented May 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This cherry-pick fixes a key-leakage bug where _safe_hash_litellm_api_key did not recognise the Bearer <key> form of an API key, causing the raw Bearer sk-... string to be emitted unhashed in observability labels (e.g. Prometheus metric tag hashed_api_key). The fix is minimal and well-scoped.

  • _types.py: _safe_hash_litellm_api_key now strips a case-insensitive Bearer prefix before applying the existing sk- / JWT hashing logic; the last return now also uses the stripped normalized value so Bearer-prefixed opaque tokens lose the prefix even if they fall through.
  • MCP auth test: Two assertions updated to match corrected behaviour — one now expects the prefix-stripped token, the other upgrades from checking a raw unhashed string to verifying the proper SHA-256 hash (strictly stronger coverage).
  • New contract test (test_proxy_types.py): Covers all four capitalisation variants of Bearer to guard against regression.

Confidence Score: 4/5

Safe to merge — the change is a focused, no-conflict cherry-pick that closes a real observability key-leak with no regressions on the changed path.

The core fix in _safe_hash_litellm_api_key is correct and the test updates reflect genuinely improved assertions (checking for a proper hash rather than a raw Bearer string). The only minor concern is that the test imports hash_token from litellm.proxy.utils while the production code calls a separate but identical copy in _types.py; if either copy diverges the test would silently pass against the wrong function. No other issues found across the three changed files.

No files require special attention; all three changed files are straightforward and the logic is easy to follow.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds Bearer-prefix stripping in _safe_hash_litellm_api_key before hashing; fix is minimal, correct, and handles case-insensitive variants.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Two assertion updates: one now expects the Bearer prefix to be stripped, the other now verifies the key is properly hashed (stricter assertion than before).
tests/test_litellm/proxy/test_proxy_types.py New contract test covering all four capitalisation variants of the Bearer prefix; verifies both api_key and token fields are normalised correctly.

Reviews (1): Last reviewed commit: "refactor(proxy/auth): normalize Bearer p..." | Re-trigger Greptile

Comment on lines 676 to +679
# Should succeed with the LiteLLM key from Authorization header
assert auth_result.api_key == "Bearer sk-litellm-valid-key"
from litellm.proxy.utils import hash_token

assert auth_result.api_key == hash_token("sk-litellm-valid-key")

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.

P2 The hash_token import inside the assertion block pulls from litellm.proxy.utils, while _safe_hash_litellm_api_key (in _types.py) calls its own local hash_token defined at line 218. Both are identical SHA-256 implementations today, so the assertion is valid — but if either copy is changed independently the test would silently diverge. Importing from _types keeps the test coupled to the same symbol the production path uses.

Suggested change
# Should succeed with the LiteLLM key from Authorization header
assert auth_result.api_key == "Bearer sk-litellm-valid-key"
from litellm.proxy.utils import hash_token
assert auth_result.api_key == hash_token("sk-litellm-valid-key")
# Should succeed with the LiteLLM key from Authorization header
from litellm.proxy._types import hash_token
assert auth_result.api_key == hash_token("sk-litellm-valid-key")

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread litellm/proxy/_types.py
return api_key
if JWTHandler.is_jwt(token=normalized):
return f"hashed-jwt-{hash_token(token=normalized)}"
return normalized

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.

P2 The fallback return normalized returns the prefix-stripped token for keys that are neither sk-* nor JWT. For an opaque Bearer <token> value that matches neither case, the old code returned Bearer <token> verbatim (unhashed), while the new code returns <token> (also unhashed). Both are technically correct — the pre-existing behaviour was already not hashing this class of input — but a clarifying comment here would make the intentional non-hashing explicit for future readers.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@yuneng-berri
yuneng-berri merged commit 8e87b53 into patch/v1.87.0rc1 May 31, 2026
2 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.

1 participant