Skip to content

[Fix] Restrict x-pass- header forwarding for credential and protocol headers#25916

Merged
yuneng-berri merged 2 commits intolitellm_yj_apr16from
litellm_xpass_header_restriction
Apr 16, 2026
Merged

[Fix] Restrict x-pass- header forwarding for credential and protocol headers#25916
yuneng-berri merged 2 commits intolitellm_yj_apr16from
litellm_xpass_header_restriction

Conversation

@yuneng-berri
Copy link
Copy Markdown
Collaborator

Summary

The x-pass- header prefix mechanism in BasePassthroughUtils.forward_headers_from_request processed all incoming headers with that prefix unconditionally, including headers whose stripped names resolve to credential or protocol-level header names (e.g. authorization, api-key, host, content-length). This allowed the forwarded value to overwrite entries already present in the outbound headers dict.

The fix introduces a small set of protected header names that are skipped during x-pass- processing. Non-protected headers continue to work as before.

Changes

Added _PROTECTED_HEADERS (authorization, api-key, host, content-length) to BasePassthroughUtils.forward_headers_from_request. Any x-pass- header whose stripped name matches a protected entry is skipped rather than written into the outbound headers dict.

Testing

  • Verified the fix against the live proxy using standalone scripts in ~/.secfix-scratch/xpass-header/ (not committed).
  • uv run pytest tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py -v — 9 passed, no regressions.

Type

🐛 Bug Fix

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 16, 2026 10:43pm

Request Review

Copy link
Copy Markdown

@veria-ai veria-ai Bot left a comment

Choose a reason for hiding this comment

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

High: Incomplete credential header protection in x-pass- header allowlist

This PR adds a blocklist to prevent x-pass- prefixed headers from overwriting certain sensitive headers (authorization, api-key, host, content-length). However, several provider-specific credential headers are missing from the protected set. An authenticated user can send x-pass-x-api-key: <value> to overwrite the Anthropic x-api-key header, or x-pass-x-goog-api-key: <value> to overwrite Google's credential header, bypassing the operator's configured API keys and the proxy's billing/spend tracking.

Comment thread litellm/passthrough/utils.py Outdated
# Always process x-pass- prefixed headers (strip prefix and forward)
# Process x-pass- prefixed headers (strip prefix and forward)
# Certain protocol-level and credential headers are excluded from this mechanism.
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High: Incomplete credential header blocklist

The protected set misses provider-specific credential headers. An authenticated user can send x-pass-x-api-key: attacker-key to overwrite the Anthropic x-api-key header set by validate_environment, or x-pass-x-goog-api-key: key for Google AI Studio. Since forward_headers_from_request processes x-pass- headers after auth headers are set (overwriting them at line 68), this lets a user substitute the operator's downstream API key with their own, bypassing spend tracking and billing controls.

Consider either switching to an allowlist approach, or at minimum adding the known provider credential headers:

Suggested change
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length"}
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length", "x-api-key", "x-goog-api-key"}

A prefix-based block on x-amz- would also prevent AWS SigV4 header tampering for Bedrock passthrough, though sign_request may re-sign after this point.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR fixes a header-injection vulnerability in BasePassthroughUtils.forward_headers_from_request where an attacker-controlled x-pass-authorization (or similar) request header could silently overwrite the proxy's outbound credential headers. The fix introduces module-level _PASS_THROUGH_PROTECTED_HEADERS and _PASS_THROUGH_PROTECTED_HEADER_PREFIXES constants, normalizes stripped header names to lowercase before writing, emits a debug log on skip, and adds a comprehensive unit test — all addressing concerns raised in the previous review cycle.

Confidence Score: 5/5

Safe to merge — security fix is correct, all previous review concerns have been addressed, and a comprehensive test suite is included.

All three P2 issues from the prior review cycle (no tests, no logging, per-call set allocation / casing inconsistency) are now resolved: constants are module-level frozensets, a debug log is emitted on skip, header keys are normalized to lowercase on write, and a full unit test covers every protected category plus the happy-path non-protected case. No new P0 or P1 issues found.

No files require special attention.

Important Files Changed

Filename Overview
litellm/passthrough/utils.py Adds module-level _PASS_THROUGH_PROTECTED_HEADERS frozenset and _PASS_THROUGH_PROTECTED_HEADER_PREFIXES tuple; modifies forward_headers_from_request to skip protected credential/protocol headers with lowercase normalization of stripped names and a debug log on skip — addresses all prior review concerns.
tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py Adds test_forward_headers_from_request_protected_headers_not_overwritten covering all six named protected headers, the x-amz-* prefix block, the still-works non-protected path, and lowercase key normalization — comprehensive coverage of the new guard.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming request header] --> B{Starts with x-pass- prefix?}
    B -- No --> Z[Skip]
    B -- Yes --> C[Strip prefix and normalize to lowercase]
    C --> D{In _PASS_THROUGH_PROTECTED_HEADERS?}
    D -- Yes --> E[Log debug and skip]
    D -- No --> F{Starts with protected prefix e.g. x-amz-?}
    F -- Yes --> E
    F -- No --> G[Write to outbound headers dict]
Loading

Reviews (2): Last reviewed commit: "fix: extend x-pass- header protection to..." | Re-trigger Greptile

Comment thread litellm/passthrough/utils.py Outdated
Comment on lines 65 to 68
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX) :]
if actual_header_name.lower() in _PROTECTED_HEADERS:
continue
headers[actual_header_name] = header_value
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 Missing tests for the new protected-header guard

No test in the committed test suite exercises the _PROTECTED_HEADERS skip path. The PR description states testing was done with standalone scripts in ~/.secfix-scratch/xpass-header/ that were not committed. This means regressions in this security fix are invisible to CI.

Per the project's testing guidelines, a test (or several) should be added to tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py covering:

  • x-pass-authorization is silently dropped (not written into headers)
  • x-pass-api-key is silently dropped
  • x-pass-host and x-pass-content-length are silently dropped
  • A non-protected x-pass-* header still works as before

Comment thread litellm/passthrough/utils.py Outdated
# Always process x-pass- prefixed headers (strip prefix and forward)
# Process x-pass- prefixed headers (strip prefix and forward)
# Certain protocol-level and credential headers are excluded from this mechanism.
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length"}
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 Silent drop may break legitimate use cases

If a user was intentionally forwarding an authorization header to a downstream service that expects a different credential than LiteLLM's own auth (e.g., x-pass-authorization: Bearer <downstream-token>), this change silently drops it without any log message, warning, or config flag to opt out. Per the project's backwards-compatibility rule, a behaviour change that drops previously-accepted input should ideally be guarded by a feature flag or at minimum log a warning so the caller knows the header was skipped.

if actual_header_name.lower() in _PROTECTED_HEADERS:
    verbose_proxy_logger.warning(
        "Skipping x-pass- header %s: maps to a protected header name",
        header_name,
    )
    continue

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment thread litellm/passthrough/utils.py Outdated
# Always process x-pass- prefixed headers (strip prefix and forward)
# Process x-pass- prefixed headers (strip prefix and forward)
# Certain protocol-level and credential headers are excluded from this mechanism.
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length"}
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 _PROTECTED_HEADERS should be a module-level constant

Defining _PROTECTED_HEADERS as a local variable inside forward_headers_from_request re-creates the set on every call and makes the list invisible to callers who might want to inspect or extend it. Moving it to module level matches common Python conventions and eliminates the per-call allocation.

Suggested change
_PROTECTED_HEADERS = {"authorization", "api-key", "host", "content-length"}
_PROTECTED_HEADERS: frozenset = frozenset(
{"authorization", "api-key", "host", "content-length"}
)
class BasePassthroughUtils:

Comment thread litellm/passthrough/utils.py Outdated
for header_name, header_value in request_headers.items():
if header_name.lower().startswith(PASS_THROUGH_HEADER_PREFIX):
# Strip the 'x-pass-' prefix to get the actual header name
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX) :]
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 Stripped header name preserves original casing when written

actual_header_name retains the casing of the incoming request header (e.g. x-pass-Anthropic-BetaAnthropic-Beta), because only the .lower() comparison against _PROTECTED_HEADERS is normalized — the value written into headers on line 68 still uses actual_header_name as-is. Two callers sending x-pass-Custom-Header and x-pass-custom-header would produce distinct keys in the output dict. If the intent is case-insensitive uniqueness in the output, normalise the key on write as well:

Suggested change
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX) :]
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX):].lower()

…headers and add tests

- Move protected-headers set to module level as a frozenset
- Add x-api-key, x-goog-api-key to protected set (provider credential headers)
- Block x-amz- prefix to cover AWS SigV4 signing headers
- Normalize forwarded header names to lowercase on write
- Log at debug level when a protected header is skipped
- Add unit test covering protected-header drop and non-protected forwarding
@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 16, 2026 22:41 — with GitHub Actions Inactive
@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 16, 2026 22:42 — with GitHub Actions Inactive
@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 16, 2026 22:42 — with GitHub Actions Inactive
@yuneng-berri yuneng-berri changed the base branch from litellm_internal_staging to litellm_yj_apr16 April 16, 2026 22:48
@yuneng-berri yuneng-berri merged commit 773ce43 into litellm_yj_apr16 Apr 16, 2026
89 of 98 checks passed
@yuneng-berri yuneng-berri deleted the litellm_xpass_header_restriction branch April 16, 2026 22:48
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/passthrough/utils.py 77.77% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

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