mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 03:36:48 +00:00
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.
This commit is contained in:
parent
50dcaba699
commit
37905a0b75
4 changed files with 36 additions and 9 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 <script> without src that is not json_script data
|
||||
INLINE_SCRIPT = re.compile(r"<script(?![^>]*\bsrc=)(?![^>]*application/json)[^>]*>")
|
||||
INLINE_HANDLER = re.compile(r"\son[a-z]+\s*=", re.I)
|
||||
|
||||
|
||||
class ContentSecurityPolicyTests(TestCase):
|
||||
def test_pages_and_redirects_carry_the_policy(self):
|
||||
|
|
@ -11,5 +20,19 @@ class ContentSecurityPolicyTests(TestCase):
|
|||
with self.subTest(url=url):
|
||||
policy = self.client.get(url)["Content-Security-Policy"]
|
||||
self.assertEqual(
|
||||
policy, "object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
|
||||
policy,
|
||||
"script-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'",
|
||||
)
|
||||
|
||||
|
||||
class TemplatesRunNoInlineScriptTests(SimpleTestCase):
|
||||
def test_no_template_has_an_inline_script_handler_or_javascript_url(self):
|
||||
base = Path(settings.BASE_DIR)
|
||||
templates = [*base.glob("*/templates/**/*.html"), *base.glob("templates/**/*.html")]
|
||||
self.assertTrue(templates)
|
||||
for path in templates:
|
||||
source = path.read_text()
|
||||
with self.subTest(template=str(path.relative_to(base))):
|
||||
self.assertIsNone(INLINE_SCRIPT.search(source))
|
||||
self.assertIsNone(INLINE_HANDLER.search(source))
|
||||
self.assertNotIn("javascript:", source)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from django.urls import include, path, re_path
|
|||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
SpectacularSwaggerSplitView,
|
||||
)
|
||||
from appsettings.views import appsettings
|
||||
from console.views import console
|
||||
|
|
@ -35,7 +35,7 @@ urlpatterns = [
|
|||
path('api/v1/', include("webvirtcloud.urls-api")),
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
re_path(r"^swagger\.(?P<format>json|yaml)$", SpectacularAPIView.as_view(), name="schema-json"),
|
||||
path("swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="schema-swagger-ui"),
|
||||
path("swagger/", SpectacularSwaggerSplitView.as_view(url_name="schema"), name="schema-swagger-ui"),
|
||||
path("redoc/", SpectacularRedocView.as_view(url_name="schema"), name="schema-redoc"),
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue