Skip to content

Commit 4f429e6

Browse files
committed
Allow frame ancestor URLs per CMS page
Valid Frame ancestor URLs are stored in the FDS page extension and are validated on save to be a space-separated list of URLs. The CSP header is then set in the middleware that also disables X-Frame-Options selectively.
1 parent 3fc28ff commit 4f429e6

File tree

3 files changed

+62
-0
lines changed

3 files changed

+62
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.14 on 2024-08-12 08:49
2+
3+
from django.db import migrations, models
4+
5+
import fragdenstaat_de.fds_cms.models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("fds_cms", "0067_alter_pretixembedcmsplugin_additional_settings"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="fdspageextension",
17+
name="frame_ancestors",
18+
field=models.CharField(
19+
blank=True,
20+
max_length=255,
21+
validators=[
22+
fragdenstaat_de.fds_cms.models.validate_space_separated_urls
23+
],
24+
verbose_name="Space separated list of allowed frame ancestor URLs.",
25+
),
26+
),
27+
]

fragdenstaat_de/fds_cms/models.py

+25
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.core.exceptions import ValidationError
23
from django.db import models
34
from django.urls import NoReverseMatch
45
from django.utils.translation import gettext_lazy as _
@@ -19,6 +20,20 @@
1920
from froide.publicbody.models import Category, Classification, Jurisdiction, PublicBody
2021

2122

23+
def validate_space_separated_urls(value):
24+
if not value:
25+
return
26+
if ";" in value:
27+
# Prevent adding different policy directives.
28+
raise ValidationError("Use space separated URLs, no semicolons allowed.")
29+
if len(value.splitlines()) > 1:
30+
# Prevent header injection.
31+
raise ValidationError("No line breaks allowed.")
32+
for url in value.split(" "):
33+
if not url.startswith("https://"):
34+
raise ValidationError("Invalid URL: %s" % url)
35+
36+
2237
@extension_pool.register
2338
class FdsPageExtension(PageExtension):
2439
search_index = models.BooleanField(
@@ -57,6 +72,16 @@ class FdsPageExtension(PageExtension):
5772
),
5873
default=False,
5974
)
75+
frame_ancestors = models.CharField(
76+
_("Space separated list of allowed frame ancestor URLs."),
77+
max_length=255,
78+
blank=True,
79+
validators=[validate_space_separated_urls],
80+
)
81+
82+
def get_frame_ancestors(self):
83+
# Split by generic whitespace
84+
return self.frame_ancestors.split()
6085

6186

6287
class PageAnnotationCMSPlugin(CMSPlugin):

fragdenstaat_de/theme/middleware.py

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
class XFrameOptionsCSPMiddleware(XFrameOptionsMiddleware):
55
def process_response(self, request, response):
66
response = super().process_response(request, response)
7+
current_page = getattr(request, "current_page", None)
8+
if current_page is not None:
9+
# current_page is a lazy object that only evaluates to None on attr access
10+
extension = getattr(current_page, "fdspageextension", None)
11+
if extension is not None and extension.frame_ancestors:
12+
frame_ancestor_urls = " ".join(extension.get_frame_ancestors())
13+
response.headers["Content-Security-Policy"] = (
14+
"frame-ancestors 'self' %s" % frame_ancestor_urls
15+
)
16+
del response["X-Frame-Options"]
717
if response.has_header("X-Frame-Options"):
818
response.headers["Content-Security-Policy"] = "frame-ancestors 'self'"
919
return response

0 commit comments

Comments
 (0)