- Disable OpenAPI docs/redoc/openapi.json when APP_ENV=production - Strip uvicorn Server header with --no-server-header in Dockerfile and docker-compose.yml - Add OriginValidatorMiddleware to reject state-changing requests from disallowed origins - Add global exception handler to prevent stack trace leakage on unhandled errors - Add tests for all new security features (OpenAPI, origin validation, exception handler, server header)
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""Unit tests for api/security_headers.py."""
|
|
from starlette.testclient import TestClient
|
|
from starlette.applications import Starlette
|
|
from starlette.requests import Request
|
|
from starlette.responses import JSONResponse
|
|
from starlette.routing import Route
|
|
|
|
from api.security_headers import SecurityHeadersMiddleware
|
|
|
|
|
|
async def _ok_endpoint(request: Request) -> JSONResponse:
|
|
return JSONResponse({"ok": True})
|
|
|
|
|
|
def _build_app() -> Starlette:
|
|
app = Starlette(routes=[Route("/test", _ok_endpoint)])
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
return app
|
|
|
|
|
|
class TestSecurityHeaders:
|
|
"""Tests for SecurityHeadersMiddleware."""
|
|
|
|
def test_x_content_type_options(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert resp.headers["X-Content-Type-Options"] == "nosniff"
|
|
|
|
def test_x_frame_options(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert resp.headers["X-Frame-Options"] == "DENY"
|
|
|
|
def test_referrer_policy(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert resp.headers["Referrer-Policy"] == "strict-origin-when-cross-origin"
|
|
|
|
def test_content_security_policy(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert "Content-Security-Policy" in resp.headers
|
|
csp = resp.headers["Content-Security-Policy"]
|
|
assert "default-src 'self'" in csp
|
|
assert "frame-ancestors 'none'" in csp
|
|
|
|
def test_hsts_set_for_https(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test", headers={"x-forwarded-proto": "https"})
|
|
assert "Strict-Transport-Security" in resp.headers
|
|
assert "max-age=" in resp.headers["Strict-Transport-Security"]
|
|
|
|
def test_hsts_not_set_for_http(self) -> None:
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert "Strict-Transport-Security" not in resp.headers
|
|
|
|
def test_server_header_not_present(self) -> None:
|
|
"""The Server header should not leak server software info."""
|
|
client = TestClient(_build_app())
|
|
resp = client.get("/test")
|
|
assert "Server" not in resp.headers or "uvicorn" not in resp.headers.get("Server", "").lower()
|