fix: extend x-pass- header protection to cover additional credential 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
This commit is contained in:
Yuneng Jiang 2026-04-16 15:41:34 -07:00
parent f46e9959db
commit 5df7c21c9a
No known key found for this signature in database
2 changed files with 83 additions and 5 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
@ -57,13 +75,19 @@ class BasePassthroughUtils:
headers = {**request_headers, **headers}
# 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"}
# 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) :]
if actual_header_name.lower() in _PROTECTED_HEADERS:
# 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

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():
"""