mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
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:
parent
f46e9959db
commit
5df7c21c9a
2 changed files with 83 additions and 5 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue