diff --git a/pdm.lock b/pdm.lock index e40d73c468e64fb34e85aa0c6c2a75aca70ddd24..1ebc77d7e4bff3efb9fe7069ad59a0e5c6ed2553 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "dev", "lint", "local", "static-analysis", "typing", "watchfiles"] +groups = ["default", "dev", "django-csp", "lint", "local", "static-analysis", "typing", "watchfiles"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:af6518bad6a153127d484181441f930ee00c15c1d0f704ea906e0fc5b4d15282" +content_hash = "sha256:1e5c1e57c2e2a93e38b0b129351d7efb840b97cca3e7b69b9277b9766e03f521" [[metadata.targets]] requires_python = "==3.13.*" @@ -472,6 +472,19 @@ files = [ {file = "django_cors_headers-4.6.0.tar.gz", hash = "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8"}, ] +[[package]] +name = "django-csp" +version = "3.8" +summary = "Django Content Security Policy support." +groups = ["default"] +dependencies = [ + "Django>=3.2", +] +files = [ + {file = "django_csp-3.8-py3-none-any.whl", hash = "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719"}, + {file = "django_csp-3.8.tar.gz", hash = "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0"}, +] + [[package]] name = "django-debug-toolbar" version = "4.4.6" diff --git a/pyproject.toml b/pyproject.toml index 9e4c869dfb62b52555721b72e47dabcd21e1f01c..48c77b34fe935b379b4349371f87cb985329fb47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "gunicorn>=23.0.0", "pydantic>=2.9.2", "django-rich>=1.13.0", + "django-csp>=3.8", "rules>=3.5", ] requires-python = "==3.13.*" diff --git a/requirements.dev.txt b/requirements.dev.txt index 7da485862cf49823ec2ef5f4c4e06a8dde9d9218..93aee3222cd3181cf681427344935cf3a125e245 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -31,6 +31,7 @@ distlib==0.3.9 django==5.1.2 django-bootstrap5==24.3 django-cors-headers==4.6.0 +django-csp==3.8 django-debug-toolbar==4.4.6 django-environ==0.11.2 django-modeltranslation==0.18.13 diff --git a/requirements.txt b/requirements.txt index 1467aba9ded099d2bcff8dfce66cd482ac459f7b..f74cfcd121fb438008f1261a2006d3d645bbce29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ distlib==0.3.9 django==5.1.2 django-bootstrap5==24.3 django-cors-headers==4.6.0 +django-csp==3.8 django-debug-toolbar==4.4.6 django-environ==0.11.2 django-modeltranslation==0.18.13 diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py index 1c4d6482e7febf1f186368b549712a29e4884c34..234f57d16e5d2cf402ba2e5f4b67d362acb3ed6f 100644 --- a/src/hub/settings/base.py +++ b/src/hub/settings/base.py @@ -123,6 +123,19 @@ env = environ.FileAwareEnv( API_USERS=(list, []), DISABLE_REQUEST_LOGGING=(bool, False), MOLLY_GUARD=(bool, True), + CSP_DEFAULT_SRC=(list, ["'self'"]), + CSP_SCRIPT_SRC=(list, ["'self'"]), + CSP_STYLE_SRC=(list, ["'self'", "'unsafe-inline'"]), + CSP_IMG_SRC=(list, ["'self'", 'data:']), + CSP_CONNECT_SRC=(list, ["'self'"]), + CSP_FONT_SRC=(list, ["'self'"]), + CSP_OBJECT_SRC=(list, ["'none'"]), + CSP_FRAME_SRC=(list, ["'none'"]), + CSP_MEDIA_SRC=(list, ["'self'"]), + CSP_FRAME_ANCESTORS=(list, ["'none'"]), + CSP_FORM_ACTION=(list, ["'self'"]), + CSP_BASE_URI=(list, ["'self'"]), + CSP_INCLUDE_NONCE_IN=(list, ['script-src']), ) @@ -195,6 +208,7 @@ INSTALLED_APPS = [ 'rest_framework', 'rest_framework.authtoken', 'django_rich', + 'csp', 'rules', # our apps 'core', @@ -211,6 +225,7 @@ MIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'csp.middleware.CSPMiddleware', 'core.middleware.TimezoneMiddleware', # TODO drĂ¼ber nachdenken ob wir die brauchen (ist default an in Django) # 'django.middleware.clickjacking.XFrameOptionsMiddleware', # noqa: ERA001 @@ -229,6 +244,7 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.i18n', + 'csp.context_processors.nonce', ], }, }, @@ -293,6 +309,21 @@ CSRF_COOKIE_NAME = env('CSRF_COOKIE_NAME', default=SESSION_COOKIE_NAME.replace(' CSRF_COOKIE_PATH = SESSION_COOKIE_PATH CSRF_COOKIE_SECURE = SESSION_COOKIE_SECURE +# Content Security Policy +CSP_DEFAULT_SRC = env('CSP_DEFAULT_SRC') +CSP_SCRIPT_SRC = env('CSP_SCRIPT_SRC') +CSP_STYLE_SRC = env('CSP_STYLE_SRC') +CSP_IMG_SRC = env('CSP_IMG_SRC') +CSP_CONNECT_SRC = env('CSP_CONNECT_SRC') +CSP_FONT_SRC = env('CSP_FONT_SRC') +CSP_OBJECT_SRC = env('CSP_OBJECT_SRC') +CSP_FRAME_SRC = env('CSP_FRAME_SRC') +CSP_MEDIA_SRC = env('CSP_MEDIA_SRC') +CSP_FRAME_ANCESTORS = env('CSP_FRAME_ANCESTORS') +CSP_FORM_ACTION = env('CSP_FORM_ACTION') +CSP_BASE_URI = env('CSP_BASE_URI') +CSP_INCLUDE_NONCE_IN = env('CSP_INCLUDE_NONCE_IN') + # OAuth2 configuration OAUTH2_PROVIDER_APPLICATION_MODEL = 'core.Application' OAUTH2_PROVIDER = { diff --git a/src/hub/settings/default.py b/src/hub/settings/default.py index f7c1ee2ba33cd603c778d9cf26a0ce3034a4f5ed..086f23d434f2fbd5ce55c3f4b7c17807b4619b62 100644 --- a/src/hub/settings/default.py +++ b/src/hub/settings/default.py @@ -138,6 +138,7 @@ if IS_FRONTEND: 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'csp.context_processors.nonce', ], 'environment': 'plainui.jinja2.environment', },