From 32459e05ca81d2ab228e95537c29f2cf4e28020c Mon Sep 17 00:00:00 2001
From: Hanne Moa <hanne.moa@sikt.no>
Date: Wed, 21 Feb 2024 13:03:13 +0100
Subject: [PATCH] Add CSP: frame-ancestors support

---
 python/nav/django/settings.py |  2 ++
 python/nav/web/security.py    | 23 +++++++++++++++++++++++
 requirements/base.txt         |  1 +
 3 files changed, 26 insertions(+)

diff --git a/python/nav/django/settings.py b/python/nav/django/settings.py
index d9aacb4ef5..ebf7e8cced 100644
--- a/python/nav/django/settings.py
+++ b/python/nav/django/settings.py
@@ -127,6 +127,7 @@
 # Middleware
 MIDDLEWARE = (
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'csp.middleware.CSPMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'nav.web.auth.middleware.AuthenticationMiddleware',
@@ -270,6 +271,7 @@
 _needs_tls = bool(_websecurity_config.getboolean('needs_tls'))
 SESSION_COOKIE_SECURE = _needs_tls
 X_FRAME_OPTIONS = _websecurity_config.get_x_frame_options()
+CSP_FRAME_ANCESTORS = _websecurity_config.get_frame_ancestors()
 
 # Hack for hackers to use features like debug_toolbar etc.
 # https://code.djangoproject.com/wiki/SplitSettings (Rob Golding's method)
diff --git a/python/nav/web/security.py b/python/nav/web/security.py
index 21a43ac968..06a0c1b5a5 100644
--- a/python/nav/web/security.py
+++ b/python/nav/web/security.py
@@ -25,3 +25,26 @@ def get_x_frame_options(self):
         if frames_flag == 'none':
             return 'DENY'
         return 'SAMEORIGIN'
+
+    def get_frame_ancestors(self):
+        """Return a list of sources
+
+        A single 'none' or a string of one or more of self, source-scheme and
+        host-scheme are valid. There is currently no validator for host-scheme,
+        so source-scheme and host-scheme are both outputted as-is.
+
+        To be set in django settings and used by the django-csp middleware.
+        """
+        default = "'self'"
+        frames_flag = self.get(self.FRAMES_OPTION) or self.FRAMES_DEFAULT
+        pieces = frames_flag.split()
+        valid_pieces = []
+        for piece in pieces:
+            if piece == 'none':
+                valid_pieces.append("'none'")
+                break
+            if piece == 'self':
+                valid_pieces.append(default)
+            else:
+                valid_pieces.append(piece)
+        return valid_pieces or [default]
diff --git a/requirements/base.txt b/requirements/base.txt
index 4af7fd589e..635ee6c624 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -23,6 +23,7 @@ dnspython<3.0.0,>=2.1.0
 django-filter>=2
 djangorestframework>=3.12,<3.13
 django-crispy-forms>=1.8,<1.9
+django-csp
 crispy-forms-foundation>=0.7,<0.8
 
 # REST framework