(back) install and configure django csp (#1085)

We want to protect all requests from django with content security
policy header. We use the djang-csp library and configure it with
default values.

Fixes #1000
This commit is contained in:
Manuel Raynaud
2025-06-30 10:42:48 +02:00
committed by GitHub
parent 4ae757ce93
commit dfdfe83db5
6 changed files with 196 additions and 107 deletions

View File

@@ -25,6 +25,8 @@ from django.utils.translation import gettext_lazy as _
import requests
import rest_framework as drf
from botocore.exceptions import ClientError
from csp.constants import NONE
from csp.decorators import csp_update
from lasuite.malware_detection import malware_detection
from rest_framework import filters, status, viewsets
from rest_framework import response as drf_response
@@ -1412,6 +1414,7 @@ class DocumentViewSet(
name="",
url_path="cors-proxy",
)
@csp_update({"img-src": [NONE, "data:"]})
def cors_proxy(self, request, *args, **kwargs):
"""
GET /api/v1.0/documents/<resource_id>/cors-proxy
@@ -1452,7 +1455,6 @@ class DocumentViewSet(
content_type=content_type,
headers={
"Content-Disposition": "attachment;",
"Content-Security-Policy": "default-src 'none'; img-src 'none' data:;",
},
status=response.status_code,
)

View File

@@ -23,10 +23,25 @@ def test_api_docs_cors_proxy_valid_url():
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
assert policy_list == [
"base-uri 'none'",
"child-src 'none'",
"connect-src 'none'",
"default-src 'none'",
"font-src 'none'",
"form-action 'none'",
"frame-ancestors 'none'",
"frame-src 'none'",
"img-src 'none' data:",
"manifest-src 'none'",
"media-src 'none'",
"object-src 'none'",
"prefetch-src 'none'",
"script-src 'none'",
"style-src 'none'",
"worker-src 'none'",
]
assert response.streaming_content
@@ -77,10 +92,25 @@ def test_api_docs_cors_proxy_authenticated_user_accessing_protected_doc():
assert response.status_code == 200
assert response.headers["Content-Type"] == "image/png"
assert response.headers["Content-Disposition"] == "attachment;"
assert (
response.headers["Content-Security-Policy"]
== "default-src 'none'; img-src 'none' data:;"
)
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
assert policy_list == [
"base-uri 'none'",
"child-src 'none'",
"connect-src 'none'",
"default-src 'none'",
"font-src 'none'",
"form-action 'none'",
"frame-ancestors 'none'",
"frame-src 'none'",
"img-src 'none' data:",
"manifest-src 'none'",
"media-src 'none'",
"object-src 'none'",
"prefetch-src 'none'",
"script-src 'none'",
"style-src 'none'",
"worker-src 'none'",
]
assert response.streaming_content

View File

@@ -62,6 +62,25 @@ def test_api_config(is_authenticated):
"AI_FEATURE_ENABLED": False,
"theme_customization": {},
}
policy_list = sorted(response.headers["Content-Security-Policy"].split("; "))
assert policy_list == [
"base-uri 'none'",
"child-src 'none'",
"connect-src 'none'",
"default-src 'none'",
"font-src 'none'",
"form-action 'none'",
"frame-ancestors 'none'",
"frame-src 'none'",
"img-src 'none'",
"manifest-src 'none'",
"media-src 'none'",
"object-src 'none'",
"prefetch-src 'none'",
"script-src 'none'",
"style-src 'none'",
"worker-src 'none'",
]
@override_settings(

View File

@@ -18,9 +18,12 @@ from django.utils.translation import gettext_lazy as _
import sentry_sdk
from configurations import Configuration, values
from csp.constants import NONE
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger
# pylint: disable=too-many-lines
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_DIR = os.getenv("DATA_DIR", os.path.join("/", "data"))
@@ -289,6 +292,7 @@ class Base(Configuration):
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"dockerflow.django.middleware.DockerflowMiddleware",
"csp.middleware.CSPMiddleware",
]
AUTHENTICATION_BACKENDS = [
@@ -322,6 +326,7 @@ class Base(Configuration):
# OIDC third party
"mozilla_django_oidc",
"lasuite.malware_detection",
"csp",
]
# Cache
@@ -721,6 +726,38 @@ class Base(Configuration):
environ_prefix=None,
)
# Content Security Policy
# See https://content-security-policy.com/ for more information.
CONTENT_SECURITY_POLICY = {
"EXCLUDE_URL_PREFIXES": values.ListValue(
[],
environ_name="CONTENT_SECURITY_POLICY_EXCLUDE_URL_PREFIXES",
environ_prefix=None,
),
"DIRECTIVES": values.DictValue(
default={
"default-src": [NONE],
"script-src": [NONE],
"style-src": [NONE],
"img-src": [NONE],
"connect-src": [NONE],
"font-src": [NONE],
"object-src": [NONE],
"media-src": [NONE],
"frame-src": [NONE],
"child-src": [NONE],
"form-action": [NONE],
"frame-ancestors": [NONE],
"base-uri": [NONE],
"worker-src": [NONE],
"manifest-src": [NONE],
"prefetch-src": [NONE],
},
environ_name="CONTENT_SECURITY_POLICY_DIRECTIVES",
environ_prefix=None,
),
}
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):

View File

@@ -32,6 +32,7 @@ dependencies = [
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-csp==4.0",
"django-filter==25.1",
"django-lasuite[all]==0.0.9",
"django-parler==2.3",