stackchain-dashboard/tests/test_security_headers.py
timmy 6e6e63e553
All checks were successful
CI / lint (pull_request) Successful in 29s
CI / build-frontend (pull_request) Successful in 4s
security: enforce strict browser execution boundary (#295)
2026-08-08 11:38:18 +00:00

60 lines
2.3 KiB
Python

import httpx
import pytest
from src import main
REQUIRED_HEADERS = {
"x-content-type-options": "nosniff",
"referrer-policy": "no-referrer",
"x-frame-options": "DENY",
}
def assert_browser_security_boundary(response: httpx.Response) -> None:
for name, value in REQUIRED_HEADERS.items():
assert response.headers[name] == value
assert "camera=()" in response.headers["permissions-policy"]
policy = response.headers["content-security-policy"]
assert "default-src 'self'" in policy
assert "script-src 'self'" in policy
assert "object-src 'none'" in policy
assert "frame-ancestors 'none'" in policy
script_policy = next(part for part in policy.split(";") if "script-src" in part)
assert "'unsafe-inline'" not in script_policy
assert "'unsafe-eval'" not in script_policy
@pytest.mark.anyio
async def test_security_boundary_covers_pages_health_static_and_api_errors(monkeypatch):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "access-token-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", "session-secret-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
responses = [
await client.get("/healthz"),
await client.get("/login"),
await client.get("/static/dashboard.js"),
await client.get("/api/v1/context"),
]
assert [response.status_code for response in responses] == [200, 200, 200, 401]
for response in responses:
assert_browser_security_boundary(response)
@pytest.mark.anyio
async def test_security_boundary_covers_fail_closed_authentication_response(monkeypatch):
monkeypatch.delenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_AUTH_MODE", raising=False)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
response = await client.get("/login")
assert response.status_code == 503
assert_browser_security_boundary(response)