From df1bf0a20903c5821ab3cc7feccf3cc80d4483c9 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:00:52 -0700 Subject: [PATCH] feat(api-server): add basic security headers (#3576) Add X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer to all API server responses via a new security_headers_middleware. Co-authored-by: Oktay Aydin --- gateway/platforms/api_server.py | 19 ++++++++++++++++++- tests/gateway/test_api_server.py | 14 +++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index a7776c0c3..7d8d81171 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -223,6 +223,23 @@ if AIOHTTP_AVAILABLE: else: body_limit_middleware = None # type: ignore[assignment] +_SECURITY_HEADERS = { + "X-Content-Type-Options": "nosniff", + "Referrer-Policy": "no-referrer", +} + + +if AIOHTTP_AVAILABLE: + @web.middleware + async def security_headers_middleware(request, handler): + """Add security headers to all responses (including errors).""" + response = await handler(request) + for k, v in _SECURITY_HEADERS.items(): + response.headers.setdefault(k, v) + return response +else: + security_headers_middleware = None # type: ignore[assignment] + class _IdempotencyCache: """In-memory idempotency cache with TTL and basic LRU semantics.""" @@ -1224,7 +1241,7 @@ class APIServerAdapter(BasePlatformAdapter): return False try: - mws = [mw for mw in (cors_middleware, body_limit_middleware) if mw is not None] + mws = [mw for mw in (cors_middleware, body_limit_middleware, security_headers_middleware) if mw is not None] self._app = web.Application(middlewares=mws) self._app["api_server_adapter"] = self self._app.router.add_get("/health", self._handle_health) diff --git a/tests/gateway/test_api_server.py b/tests/gateway/test_api_server.py index e40902a58..772dd8b1c 100644 --- a/tests/gateway/test_api_server.py +++ b/tests/gateway/test_api_server.py @@ -28,6 +28,7 @@ from gateway.platforms.api_server import ( _CORS_HEADERS, check_api_server_requirements, cors_middleware, + security_headers_middleware, ) @@ -214,7 +215,8 @@ def _make_adapter(api_key: str = "", cors_origins=None) -> APIServerAdapter: def _create_app(adapter: APIServerAdapter) -> web.Application: """Create the aiohttp app from the adapter (without starting the full server).""" - app = web.Application(middlewares=[cors_middleware]) + mws = [mw for mw in (cors_middleware, security_headers_middleware) if mw is not None] + app = web.Application(middlewares=mws) app["api_server_adapter"] = adapter app.router.add_get("/health", adapter._handle_health) app.router.add_get("/v1/health", adapter._handle_health) @@ -242,6 +244,16 @@ def auth_adapter(): class TestHealthEndpoint: + @pytest.mark.asyncio + async def test_security_headers_present(self, adapter): + """Responses should include basic security headers.""" + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.get("/health") + assert resp.status == 200 + assert resp.headers.get("X-Content-Type-Options") == "nosniff" + assert resp.headers.get("Referrer-Policy") == "no-referrer" + @pytest.mark.asyncio async def test_health_returns_ok(self, adapter): app = _create_app(adapter)