68 lines
2.4 KiB
Python
68 lines
2.4 KiB
Python
"""Pin the Control UI Content-Security-Policy.
|
|
|
|
The chat surface previews artifacts (notably generated images) by fetching the
|
|
authenticated bytes and rendering an object URL (``blob:``) in an ``<img>``.
|
|
If ``img-src`` omits ``blob:`` the browser blocks every generated-image
|
|
preview while the file still downloads fine — a "the UI lied" failure. These
|
|
tests keep ``blob:`` in the policy and keep the header scoped to the Control UI
|
|
path prefix.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from starlette.applications import Starlette
|
|
from starlette.responses import PlainTextResponse
|
|
from starlette.routing import Route
|
|
from starlette.testclient import TestClient
|
|
|
|
from opensquilla.gateway.middleware import SecurityHeadersMiddleware
|
|
|
|
|
|
def _client(path_prefix: str = "/control") -> TestClient:
|
|
async def ok(_request): # type: ignore[no-untyped-def]
|
|
return PlainTextResponse("ok")
|
|
|
|
app = Starlette(
|
|
routes=[
|
|
Route("/control/ping", ok),
|
|
Route("/other", ok),
|
|
]
|
|
)
|
|
app.add_middleware(SecurityHeadersMiddleware, path_prefix=path_prefix)
|
|
return TestClient(app)
|
|
|
|
|
|
def test_csp_allows_blob_images_for_artifact_previews() -> None:
|
|
response = _client().get("/control/ping")
|
|
|
|
assert response.status_code == 200
|
|
csp = response.headers.get("content-security-policy", "")
|
|
assert "img-src 'self' data: blob:;" in csp, csp
|
|
assert "font-src 'self' data:;" in csp, csp
|
|
|
|
|
|
def test_csp_still_constrains_default_and_connect_sources() -> None:
|
|
csp = _client().get("/control/ping").headers.get("content-security-policy", "")
|
|
|
|
# blob: is added only to img-src; the rest of the policy stays locked down.
|
|
assert "default-src 'self';" in csp, csp
|
|
assert "connect-src 'self' ws: wss:;" in csp, csp
|
|
assert "blob:" not in csp.split("img-src", 1)[0], csp
|
|
|
|
|
|
def test_csp_allows_only_same_origin_blob_and_https_media() -> None:
|
|
csp = _client().get("/control/ping").headers.get("content-security-policy", "")
|
|
directives = [directive.strip() for directive in csp.split(";") if directive.strip()]
|
|
|
|
assert [
|
|
directive for directive in directives if directive.startswith("media-src ")
|
|
] == ["media-src 'self' blob: https:"], csp
|
|
|
|
|
|
def test_security_headers_scoped_to_control_prefix() -> None:
|
|
response = _client().get("/other")
|
|
|
|
assert response.status_code == 200
|
|
assert "content-security-policy" not in response.headers
|
|
assert "x-frame-options" not in response.headers
|