Harden HTML5 response headers
This commit is contained in:
@@ -135,12 +135,14 @@ from ui_semantics import form_semantics
|
|||||||
|
|
||||||
app = FastAPI(title="SFERA API", version="0.1.0")
|
app = FastAPI(title="SFERA API", version="0.1.0")
|
||||||
_HTML5_ASSETS_DIR = Path(__file__).resolve().parent / "static" / "html5"
|
_HTML5_ASSETS_DIR = Path(__file__).resolve().parent / "static" / "html5"
|
||||||
|
_HTML5_SECURITY_HEADERS = {"X-Content-Type-Options": "nosniff"}
|
||||||
|
|
||||||
|
|
||||||
class Html5StaticFiles(StaticFiles):
|
class Html5StaticFiles(StaticFiles):
|
||||||
def file_response(self, *args, **kwargs):
|
def file_response(self, *args, **kwargs):
|
||||||
response = super().file_response(*args, **kwargs)
|
response = super().file_response(*args, **kwargs)
|
||||||
response.headers.setdefault("Cache-Control", "public, max-age=86400")
|
response.headers.setdefault("Cache-Control", "public, max-age=86400")
|
||||||
|
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -8438,6 +8440,7 @@ def _html5_sse_headers() -> dict[str, str]:
|
|||||||
"Cache-Control": "no-cache, no-transform",
|
"Cache-Control": "no-cache, no-transform",
|
||||||
"Connection": "keep-alive",
|
"Connection": "keep-alive",
|
||||||
"X-Accel-Buffering": "no",
|
"X-Accel-Buffering": "no",
|
||||||
|
**_HTML5_SECURITY_HEADERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -8445,7 +8448,7 @@ def _html5_response(fragment: str) -> Response:
|
|||||||
return Response(
|
return Response(
|
||||||
fragment,
|
fragment,
|
||||||
media_type="text/html; charset=utf-8",
|
media_type="text/html; charset=utf-8",
|
||||||
headers={"Cache-Control": "no-cache, no-transform"},
|
headers={"Cache-Control": "no-cache, no-transform", **_HTML5_SECURITY_HEADERS},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ def assert_html5_contract(text: str, *markers: str, full_page: bool = False) ->
|
|||||||
def assert_html5_response_contract(response, *markers: str, full_page: bool = False) -> None:
|
def assert_html5_response_contract(response, *markers: str, full_page: bool = False) -> None:
|
||||||
assert "text/html" in response.headers["content-type"]
|
assert "text/html" in response.headers["content-type"]
|
||||||
assert response.headers["cache-control"] == "no-cache, no-transform"
|
assert response.headers["cache-control"] == "no-cache, no-transform"
|
||||||
|
assert response.headers["x-content-type-options"] == "nosniff"
|
||||||
assert_html5_contract(response.text, *markers, full_page=full_page)
|
assert_html5_contract(response.text, *markers, full_page=full_page)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,6 +177,7 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
|||||||
assert events.headers["cache-control"] == "no-cache, no-transform"
|
assert events.headers["cache-control"] == "no-cache, no-transform"
|
||||||
assert events.headers["x-accel-buffering"] == "no"
|
assert events.headers["x-accel-buffering"] == "no"
|
||||||
assert events.headers["connection"] == "keep-alive"
|
assert events.headers["connection"] == "keep-alive"
|
||||||
|
assert events.headers["x-content-type-options"] == "nosniff"
|
||||||
first_chunk = "".join(events.iter_text())
|
first_chunk = "".join(events.iter_text())
|
||||||
assert ": project " in first_chunk
|
assert ": project " in first_chunk
|
||||||
assert "retry: 5000" in first_chunk
|
assert "retry: 5000" in first_chunk
|
||||||
@@ -267,16 +269,19 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
|||||||
assert htmx_asset.status_code == 200
|
assert htmx_asset.status_code == 200
|
||||||
assert "javascript" in htmx_asset.headers["content-type"]
|
assert "javascript" in htmx_asset.headers["content-type"]
|
||||||
assert htmx_asset.headers["cache-control"] == "public, max-age=86400"
|
assert htmx_asset.headers["cache-control"] == "public, max-age=86400"
|
||||||
|
assert htmx_asset.headers["x-content-type-options"] == "nosniff"
|
||||||
assert "htmx" in htmx_asset.text
|
assert "htmx" in htmx_asset.text
|
||||||
sse_asset = client.get("/html5/assets/htmx-ext-sse.js")
|
sse_asset = client.get("/html5/assets/htmx-ext-sse.js")
|
||||||
assert sse_asset.status_code == 200
|
assert sse_asset.status_code == 200
|
||||||
assert "javascript" in sse_asset.headers["content-type"]
|
assert "javascript" in sse_asset.headers["content-type"]
|
||||||
assert sse_asset.headers["cache-control"] == "public, max-age=86400"
|
assert sse_asset.headers["cache-control"] == "public, max-age=86400"
|
||||||
|
assert sse_asset.headers["x-content-type-options"] == "nosniff"
|
||||||
assert "sse" in sse_asset.text
|
assert "sse" in sse_asset.text
|
||||||
css_asset = client.get("/html5/assets/html5.css")
|
css_asset = client.get("/html5/assets/html5.css")
|
||||||
assert css_asset.status_code == 200
|
assert css_asset.status_code == 200
|
||||||
assert "text/css" in css_asset.headers["content-type"]
|
assert "text/css" in css_asset.headers["content-type"]
|
||||||
assert css_asset.headers["cache-control"] == "public, max-age=86400"
|
assert css_asset.headers["cache-control"] == "public, max-age=86400"
|
||||||
|
assert css_asset.headers["x-content-type-options"] == "nosniff"
|
||||||
assert ".workspace" in css_asset.text
|
assert ".workspace" in css_asset.text
|
||||||
|
|
||||||
|
|
||||||
@@ -720,6 +725,7 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert events.headers["cache-control"] == "no-cache, no-transform"
|
assert events.headers["cache-control"] == "no-cache, no-transform"
|
||||||
assert events.headers["x-accel-buffering"] == "no"
|
assert events.headers["x-accel-buffering"] == "no"
|
||||||
assert events.headers["connection"] == "keep-alive"
|
assert events.headers["connection"] == "keep-alive"
|
||||||
|
assert events.headers["x-content-type-options"] == "nosniff"
|
||||||
first_chunk = "".join(events.iter_text())
|
first_chunk = "".join(events.iter_text())
|
||||||
assert ": setup " in first_chunk
|
assert ": setup " in first_chunk
|
||||||
assert "retry: 5000" in first_chunk
|
assert "retry: 5000" in first_chunk
|
||||||
@@ -762,6 +768,7 @@ def test_html5_operations_renders_job_monitor_fragments():
|
|||||||
assert events.headers["cache-control"] == "no-cache, no-transform"
|
assert events.headers["cache-control"] == "no-cache, no-transform"
|
||||||
assert events.headers["x-accel-buffering"] == "no"
|
assert events.headers["x-accel-buffering"] == "no"
|
||||||
assert events.headers["connection"] == "keep-alive"
|
assert events.headers["connection"] == "keep-alive"
|
||||||
|
assert events.headers["x-content-type-options"] == "nosniff"
|
||||||
first_chunk = "".join(events.iter_text())
|
first_chunk = "".join(events.iter_text())
|
||||||
assert ": operations heartbeat" in first_chunk
|
assert ": operations heartbeat" in first_chunk
|
||||||
assert "retry: 5000" in first_chunk
|
assert "retry: 5000" in first_chunk
|
||||||
|
|||||||
Reference in New Issue
Block a user