Version banner, File locations section, final CSP (no unsafe-inline styles, frame-src none, object-src none)
The MTR viewer draws PDFs with pdf.js instead of an iframe the server's frame-ancestors refused. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -29,19 +29,21 @@ def _assert_headers(r) -> None: # type: ignore[no-untyped-def]
|
||||
for k, v in EXPECTED.items():
|
||||
assert r.headers.get(k) == v, k
|
||||
csp = r.headers["content-security-policy"]
|
||||
for part in (
|
||||
assert [p.strip() for p in csp.split(";")] == [
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data: blob:",
|
||||
"style-src 'self'",
|
||||
"img-src 'self' blob:",
|
||||
"font-src 'self'",
|
||||
"connect-src 'self'",
|
||||
"worker-src 'self' blob:",
|
||||
"worker-src 'self'",
|
||||
"frame-src 'none'",
|
||||
"object-src 'none'",
|
||||
"frame-ancestors 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
):
|
||||
assert part in csp
|
||||
]
|
||||
assert "unsafe-inline" not in csp and "unsafe-eval" not in csp
|
||||
assert csp == security.CSP
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""The CSP per response type: the strict policy for pages and JSON, object-src 'self' only for PDFs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import HTMLResponse, Response
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from packetstation import security
|
||||
|
||||
|
||||
def _client() -> TestClient:
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/page")
|
||||
def page() -> HTMLResponse:
|
||||
return HTMLResponse("<!doctype html><title>x</title>")
|
||||
|
||||
@app.get("/_blob/doc")
|
||||
def pdf() -> Response:
|
||||
return Response(b"%PDF-1.7\n", media_type="application/pdf")
|
||||
|
||||
@app.get("/api/print")
|
||||
def api_pdf() -> Response:
|
||||
return Response(b"%PDF-1.7\n", media_type="Application/PDF; charset=binary")
|
||||
|
||||
app.add_middleware(security.SecurityHeadersMiddleware)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _directives(csp: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for part in csp.split(";"):
|
||||
name, _, value = part.strip().partition(" ")
|
||||
out[name] = value
|
||||
return out
|
||||
|
||||
|
||||
def test_page_gets_the_strict_policy() -> None:
|
||||
r = _client().get("/page")
|
||||
csp = r.headers["content-security-policy"]
|
||||
assert csp == security.CSP
|
||||
d = _directives(csp)
|
||||
assert d["style-src"] == "'self'"
|
||||
assert d["object-src"] == "'none'"
|
||||
assert d["frame-src"] == "'none'"
|
||||
assert d["frame-ancestors"] == "'none'"
|
||||
assert r.headers["x-frame-options"] == "DENY"
|
||||
|
||||
|
||||
def test_pdf_gets_object_src_self_and_nothing_else_changes() -> None:
|
||||
c = _client()
|
||||
for path in ("/_blob/doc", "/api/print"):
|
||||
r = c.get(path)
|
||||
csp = r.headers["content-security-policy"]
|
||||
assert csp == security.PDF_CSP, path
|
||||
strict, pdf = _directives(security.CSP), _directives(csp)
|
||||
assert pdf.pop("object-src") == "'self'"
|
||||
strict.pop("object-src")
|
||||
assert pdf == strict
|
||||
assert r.headers["x-frame-options"] == "DENY"
|
||||
assert c.get("/api/print").headers["cache-control"] == "no-store"
|
||||
Reference in New Issue
Block a user