This commit is contained in:
Nabi Bukhsh 2026-10-17 08:01:34 -07:00 • committed by GitHub
commit d8947cf2dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 2 deletions

View file

@ -8,6 +8,10 @@ X-Content-Type-Options: nosniff stops MIME sniffing.
Strict-Transport-Security is opt-in via LITELLM_ENABLE_HSTS because it only
makes sense over HTTPS and would lock browsers out of plain-http deployments.
Cache-Control: no-store is opt-in via LITELLM_ENABLE_NO_STORE to prevent
browsers and intermediaries from caching sensitive responses (e.g. key lists,
spend logs, user credentials).
Headers are set with setdefault so a route that intentionally sets its own
value is never overridden.
"""
@ -24,12 +28,17 @@ STATIC_SECURITY_HEADERS: Final = (
("X-Content-Type-Options", "nosniff"),
)
HSTS_HEADER: Final = ("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
NO_STORE_HEADER: Final = ("Cache-Control", "no-store")
def _hsts_enabled() -> bool:
return os.getenv("LITELLM_ENABLE_HSTS", "false").strip().lower() == "true"
def _no_store_enabled() -> bool:
return os.getenv("LITELLM_ENABLE_NO_STORE", "false").strip().lower() == "true"
class SecurityHeadersMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
@ -42,7 +51,11 @@ class SecurityHeadersMiddleware:
async def send_with_security_headers(message: Message) -> None:
if message["type"] == "http.response.start":
headers: Final = MutableHeaders(scope=message)
applied: Final = (*STATIC_SECURITY_HEADERS, HSTS_HEADER) if _hsts_enabled() else STATIC_SECURITY_HEADERS
applied: Final = (
*STATIC_SECURITY_HEADERS,
*((HSTS_HEADER,) if _hsts_enabled() else ()),
*((NO_STORE_HEADER,) if _no_store_enabled() else ()),
)
for name, value in applied:
headers.setdefault(name, value)
await send(message)

View file

@ -32,6 +32,7 @@ EXCLUDED_ROLLOUT_FLAGS = {
"LITELLM_USE_RUST_OCR",
"LITELLM_RUST",
"LITELLM_BUILTIN_PASS_THROUGH_ROUTES_FIRST",
"LITELLM_ENABLE_NO_STORE",
}
# Internal infrastructure tuning parameters for streaming/queue management

View file

@ -2,9 +2,12 @@
Tests for SecurityHeadersMiddleware.
Verifies anti-framing / content-type headers are present on every response and
that HSTS is opt-in via LITELLM_ENABLE_HSTS.
that HSTS is opt-in via LITELLM_ENABLE_HSTS and Cache-Control: no-store is
opt-in via LITELLM_ENABLE_NO_STORE.
"""
from typing import Final
from starlette.applications import Starlette
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse, Response
@ -69,3 +72,45 @@ def test_does_not_override_existing_header(monkeypatch):
assert resp.headers["x-frame-options"] == "SAMEORIGIN"
# other headers still applied
assert resp.headers["x-content-type-options"] == "nosniff"
def test_no_store_absent_by_default(monkeypatch):
monkeypatch.delenv("LITELLM_ENABLE_NO_STORE", raising=False)
resp: Final = _make_client(_ok).get("/")
assert "cache-control" not in resp.headers
def test_no_store_present_when_enabled(monkeypatch):
monkeypatch.setenv("LITELLM_ENABLE_NO_STORE", "true")
resp: Final = _make_client(_ok).get("/")
assert resp.headers["cache-control"] == "no-store"
def test_no_store_not_enabled_by_arbitrary_value(monkeypatch):
monkeypatch.setenv("LITELLM_ENABLE_NO_STORE", "1")
resp: Final = _make_client(_ok).get("/")
assert "cache-control" not in resp.headers
def test_no_store_does_not_override_existing_header(monkeypatch):
"""A route that sets its own Cache-Control must win."""
async def custom(request):
return Response("hi", headers={"Cache-Control": "private, max-age=60"})
monkeypatch.setenv("LITELLM_ENABLE_NO_STORE", "true")
resp: Final = _make_client(custom).get("/")
assert resp.headers["cache-control"] == "private, max-age=60"
assert resp.headers["x-content-type-options"] == "nosniff"
def test_both_hsts_and_no_store_enabled(monkeypatch):
monkeypatch.setenv("LITELLM_ENABLE_HSTS", "true")
monkeypatch.setenv("LITELLM_ENABLE_NO_STORE", "true")
resp: Final = _make_client(_ok).get("/")
assert resp.headers["strict-transport-security"] == (
"max-age=31536000; includeSubDomains"
)
assert resp.headers["cache-control"] == "no-store"
assert resp.headers["x-frame-options"] == "DENY"