diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 91907a6..487934f 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -135,12 +135,14 @@ from ui_semantics import form_semantics app = FastAPI(title="SFERA API", version="0.1.0") _HTML5_ASSETS_DIR = Path(__file__).resolve().parent / "static" / "html5" +_HTML5_SECURITY_HEADERS = {"X-Content-Type-Options": "nosniff"} class Html5StaticFiles(StaticFiles): def file_response(self, *args, **kwargs): response = super().file_response(*args, **kwargs) response.headers.setdefault("Cache-Control", "public, max-age=86400") + response.headers.setdefault("X-Content-Type-Options", "nosniff") return response @@ -8438,6 +8440,7 @@ def _html5_sse_headers() -> dict[str, str]: "Cache-Control": "no-cache, no-transform", "Connection": "keep-alive", "X-Accel-Buffering": "no", + **_HTML5_SECURITY_HEADERS, } @@ -8445,7 +8448,7 @@ def _html5_response(fragment: str) -> Response: return Response( fragment, media_type="text/html; charset=utf-8", - headers={"Cache-Control": "no-cache, no-transform"}, + headers={"Cache-Control": "no-cache, no-transform", **_HTML5_SECURITY_HEADERS}, ) diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 98aabd0..b1977a0 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -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: assert "text/html" in response.headers["content-type"] 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) @@ -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["x-accel-buffering"] == "no" assert events.headers["connection"] == "keep-alive" + assert events.headers["x-content-type-options"] == "nosniff" first_chunk = "".join(events.iter_text()) assert ": project " 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 "javascript" in htmx_asset.headers["content-type"] 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 sse_asset = client.get("/html5/assets/htmx-ext-sse.js") assert sse_asset.status_code == 200 assert "javascript" in sse_asset.headers["content-type"] 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 css_asset = client.get("/html5/assets/html5.css") assert css_asset.status_code == 200 assert "text/css" in css_asset.headers["content-type"] 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 @@ -720,6 +725,7 @@ def test_html5_project_setup_renders_server_fragments(): assert events.headers["cache-control"] == "no-cache, no-transform" assert events.headers["x-accel-buffering"] == "no" assert events.headers["connection"] == "keep-alive" + assert events.headers["x-content-type-options"] == "nosniff" first_chunk = "".join(events.iter_text()) assert ": setup " 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["x-accel-buffering"] == "no" assert events.headers["connection"] == "keep-alive" + assert events.headers["x-content-type-options"] == "nosniff" first_chunk = "".join(events.iter_text()) assert ": operations heartbeat" in first_chunk assert "retry: 5000" in first_chunk