diff --git a/server.py b/server.py index 610527e1..ce003d64 100644 --- a/server.py +++ b/server.py @@ -200,6 +200,27 @@ class Handler(BaseHTTPRequestHandler): pass _ver_suffix = WEBUI_VERSION.removeprefix('v') server_version = ('HermesWebUI/' + _ver_suffix) if _ver_suffix != 'unknown' else 'HermesWebUI' + _CSP_REPORT_ONLY = ( + "default-src 'self'; " + "base-uri 'self'; " + "object-src 'none'; " + "frame-ancestors 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: blob:; " + "font-src 'self' data:; " + "media-src 'self' data: blob:; " + "connect-src 'self' http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*" + ) + + @classmethod + def csp_report_only_policy(cls) -> str: + return cls._CSP_REPORT_ONLY + + def end_headers(self) -> None: + self.send_header("Content-Security-Policy-Report-Only", self.csp_report_only_policy()) + super().end_headers() + def log_message(self, fmt, *args): pass # suppress default Apache-style log def log_request(self, code: str='-', size: str='-') -> None: diff --git a/tests/test_issue1909_csp_report_only.py b/tests/test_issue1909_csp_report_only.py new file mode 100644 index 00000000..2c22a56a --- /dev/null +++ b/tests/test_issue1909_csp_report_only.py @@ -0,0 +1,32 @@ +"""Regression tests for #1909 CSP report-only security header.""" + +from http.server import BaseHTTPRequestHandler + +from server import Handler + + +def test_handler_adds_content_security_policy_report_only(monkeypatch): + sent_headers = [] + handler = Handler.__new__(Handler) + handler.send_header = lambda key, value: sent_headers.append((key, value)) + monkeypatch.setattr(BaseHTTPRequestHandler, "end_headers", lambda self: None) + + Handler.end_headers(handler) + + headers = dict(sent_headers) + assert "Content-Security-Policy-Report-Only" in headers + assert "Content-Security-Policy" not in headers + policy = headers["Content-Security-Policy-Report-Only"] + assert "default-src 'self'" in policy + assert "object-src 'none'" in policy + assert "frame-ancestors 'self'" in policy + assert "base-uri 'self'" in policy + + +def test_csp_report_only_keeps_legacy_inline_allowances_for_current_ui(): + policy = Handler.csp_report_only_policy() + + assert "script-src 'self' 'unsafe-inline' 'unsafe-eval'" in policy + assert "style-src 'self' 'unsafe-inline'" in policy + assert "img-src 'self' data: blob:" in policy + assert "connect-src 'self'" in policy