Merge pull request #25916 from BerriAI/litellm_xpass_header_restriction

[Fix] Restrict x-pass- header forwarding for credential and protocol headers
This commit is contained in:
yuneng-jiang 2026-04-16 15:48:27 -07:00 • committed by GitHub
commit 773ce4388f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 85 additions and 3 deletions

View file

@ -3,8 +3,26 @@ from urllib.parse import parse_qs
import httpx
from litellm._logging import verbose_logger
from litellm.constants import PASS_THROUGH_HEADER_PREFIX
# Headers that must not be overwritten via the x-pass- forwarding mechanism.
# Includes standard credential/auth headers and protocol-level headers that
# affect routing or message framing.
_PASS_THROUGH_PROTECTED_HEADERS: frozenset = frozenset(
{
"authorization",
"api-key",
"x-api-key",
"x-goog-api-key",
"host",
"content-length",
}
)
# Header name prefix used to block AWS SigV4 signing headers from being overridden.
_PASS_THROUGH_PROTECTED_HEADER_PREFIXES: tuple = ("x-amz-",)
class BasePassthroughUtils:
@staticmethod
@ -56,11 +74,21 @@ class BasePassthroughUtils:
# Combine request headers with custom headers
headers = {**request_headers, **headers}
# Always process x-pass- prefixed headers (strip prefix and forward)
# Process x-pass- prefixed headers (strip prefix and forward)
# Credential and protocol-level headers are excluded from this mechanism.
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) :]
# Strip the 'x-pass-' prefix and normalize to lowercase
actual_header_name = header_name[len(PASS_THROUGH_HEADER_PREFIX) :].lower()
if actual_header_name in _PASS_THROUGH_PROTECTED_HEADERS or any(
actual_header_name.startswith(p)
for p in _PASS_THROUGH_PROTECTED_HEADER_PREFIXES
):
verbose_logger.debug(
"x-pass- header %s maps to a protected header name; skipping",
header_name,
)
continue
headers[actual_header_name] = header_value
return headers

View file

@ -451,6 +451,60 @@ def test_forward_headers_from_request_x_pass_prefix():
assert "x-pass-custom-header" not in result
def test_forward_headers_from_request_protected_headers_not_overwritten():
"""
Test that x-pass- headers whose stripped names resolve to credential or
protocol-level header names are silently dropped and do not overwrite
values already present in the outbound headers dict.
"""
from litellm.passthrough.utils import BasePassthroughUtils
proxy_headers = {
"authorization": "Bearer proxy-upstream-key",
"api-key": "proxy-azure-key",
"x-api-key": "proxy-anthropic-key",
"x-goog-api-key": "proxy-google-key",
}
request_headers = {
"x-pass-authorization": "Bearer attacker-key",
"x-pass-api-key": "attacker-azure-key",
"x-pass-x-api-key": "attacker-anthropic-key",
"x-pass-x-goog-api-key": "attacker-google-key",
"x-pass-host": "evil.example.com",
"x-pass-content-length": "0",
"x-pass-x-amz-security-token": "attacker-aws-token",
# Legitimate x-pass- header that should still be forwarded
"x-pass-anthropic-beta": "context-1m-2025-08-07",
"content-type": "application/json",
}
result = BasePassthroughUtils.forward_headers_from_request(
request_headers=request_headers,
headers=proxy_headers.copy(),
forward_headers=False,
)
# Protected headers must retain the proxy-configured values
assert result["authorization"] == "Bearer proxy-upstream-key"
assert result["api-key"] == "proxy-azure-key"
assert result["x-api-key"] == "proxy-anthropic-key"
assert result["x-goog-api-key"] == "proxy-google-key"
# Protocol headers must not be injected
assert "host" not in result
assert "content-length" not in result
# AWS SigV4 headers must not be injected
assert "x-amz-security-token" not in result
# Legitimate non-protected x-pass- header still forwarded
assert result["anthropic-beta"] == "context-1m-2025-08-07"
# Header name must be normalized to lowercase in output
assert "Anthropic-Beta" not in result
@pytest.mark.asyncio
async def test_vertex_passthrough_custom_model_name_replaced_in_url():
"""