From 37905a0b75f0382649d3b13d204370faec763c2c Mon Sep 17 00:00:00 2001 From: catborise Date: Mon, 2 Nov 2026 10:08:09 +0300 Subject: [PATCH] feat(webvirtcloud): pages run only the scripts the panel serves The Content-Security-Policy now also sends script-src 'self': a script injected into a page, inline or as an event handler, does not run. The templates have none of their own any more, and a test keeps it so. worker-src 'self' blob: lets the XML editor (Ace) and the API docs (Redoc) start their workers, which they make from blob: URLs. The Swagger UI page is drf-spectacular's split view, which serves its start script as a file instead of inline. --- README.md | 2 +- webvirtcloud/middleware.py | 8 ++++++-- webvirtcloud/tests/test_csp.py | 31 +++++++++++++++++++++++++++---- webvirtcloud/urls.py | 4 ++-- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8890a49b..aa8dd313 100644 --- a/README.md +++ b/README.md @@ -330,7 +330,7 @@ For a panel served over HTTPS: TLS can end at the bundled nginx or at a proxy in front of it; such a proxy must send `X-Forwarded-Proto: https`. Failed logins are counted per client address, which the bundled nginx passes in `X-Real-IP`. A proxy that replaces the bundled nginx must set `X-Real-IP` itself, and its address must be in `LOGIN_TRUSTED_PROXIES` in `settings.py`. With a proxy in front of the bundled nginx, configure nginx's `set_real_ip_from` / `real_ip_header` for that proxy, or all clients share its address. -Every page is sent with `X-Frame-Options: DENY` and `Content-Security-Policy: object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'`: the panel cannot be shown in a frame, and its forms post only to its own origin. +Every page is sent with `X-Frame-Options: DENY` and `Content-Security-Policy: script-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'`: the panel runs only scripts it serves itself, cannot be shown in a frame, and its forms post only to its own origin. ### Cloud-init datasource diff --git a/webvirtcloud/middleware.py b/webvirtcloud/middleware.py index f029476a..1f653b1f 100644 --- a/webvirtcloud/middleware.py +++ b/webvirtcloud/middleware.py @@ -8,8 +8,12 @@ from operations import claims, jobs from operations.utils import busy_message, public_error_text, summary API_PREFIX = "/api/" -# the directives that need no change to the templates' inline scripts and styles -CONTENT_SECURITY_POLICY = "object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" +# scripts only from this site: no inline script or event handler runs (inline styles still do); +# the XML editor (Ace) and the API docs (Redoc) start their workers from blob: URLs +CONTENT_SECURITY_POLICY = ( + "script-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; " + "frame-ancestors 'none'" +) def same_origin_referer(request, default): diff --git a/webvirtcloud/tests/test_csp.py b/webvirtcloud/tests/test_csp.py index ac6998a0..a7dff24e 100644 --- a/webvirtcloud/tests/test_csp.py +++ b/webvirtcloud/tests/test_csp.py @@ -1,9 +1,18 @@ -"""Every response carries the Content-Security-Policy directives that need no -change to the templates' inline scripts and styles.""" +"""Every response carries the Content-Security-Policy, and no template has +what its script-src 'self' would block: an inline script, an inline event +handler or a javascript: URL.""" -from django.test import TestCase +import re +from pathlib import Path + +from django.conf import settings +from django.test import SimpleTestCase, TestCase from django.urls import reverse +# a