From 4b3d31c73c5e1a6c823547a34d58cafb5b019d47 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 23:22:47 +0000 Subject: [PATCH] security(exceptions): do not auto-copy vendor response headers to e.headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A vendor 429 response can set arbitrary headers (Set-Cookie, CORS overrides, …). Previously, when RateLimitError was constructed with only a 'response=' (no explicit 'headers=' kwarg), self.headers fell back to a copy of response.headers. If a downstream proxy serializer ever forwarded e.headers to the client, a malicious upstream could inject browser-interpreted headers for the proxy origin. Drop the fallback. Only headers passed explicitly via the headers= kwarg make it onto self.headers (proxy hooks pass retry-after etc. — they control what's surfaced). Vendor response headers stay reachable on e.response.headers for callers that explicitly want them. Today's proxy_server.py route handlers don't actually forward e.headers on the wire (they construct ProxyException without passing headers), so no current behavior changes — this is a defensive narrowing so the fallback can never be turned into a vector when someone wires e.headers through later. Veria-AI security review feedback on PR #27687. LIT-2968 Co-authored-by: Mateo Wang --- litellm/exceptions.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/litellm/exceptions.py b/litellm/exceptions.py index 6c48b5bdb9d..4f319842119 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -390,15 +390,25 @@ class RateLimitError(openai.RateLimitError): # type: ignore self.category = ( category.value if isinstance(category, RateLimitErrorCategory) else category ) - # Headers carried with the error (e.g. retry-after, rate_limit_type, - # reset_at). Preserved across the proxy boundary so clients can react - # appropriately. + # Headers explicitly attached to the error (e.g. retry-after, + # rate_limit_type, reset_at). Preserved across the proxy boundary so + # clients can react appropriately. + # + # IMPORTANT: we deliberately do NOT auto-populate self.headers from + # response.headers when only `response` is provided. A vendor 429 can + # set arbitrary response headers (Set-Cookie, CORS overrides, …); if + # those leaked into e.headers and a downstream proxy serializer + # forwarded them to the client, a malicious upstream could inject + # browser-interpreted headers for the proxy origin. Vendor response + # headers stay reachable on `e.response.headers` for callers that + # explicitly want them; only the proxy-supplied `headers=` kwarg + # makes it onto `self.headers`. _response_headers = ( getattr(response, "headers", None) if response is not None else None ) self.headers: Optional[Dict[str, str]] = ( {k: str(v) for k, v in headers.items()} if headers else None - ) or (dict(_response_headers) if _response_headers else None) + ) # Mirrors FastAPI HTTPException.detail so the same instance can be # serialized through both the ProxyException and HTTPException paths. self.detail = detail if detail is not None else self.message