From 20f0bb7bbc1faa0b9436574c4662c101e42d7f1d Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 4 Dec 2025 18:37:07 +1100 Subject: [PATCH 1/3] Add type stubs for built-in CSP support in Django 6.0 --- django-stubs/conf/global_settings.pyi | 6 +++++ django-stubs/middleware/csp.pyi | 7 ++++++ django-stubs/template/context_processors.pyi | 1 + django-stubs/utils/csp.pyi | 24 ++++++++++++++++++++ django-stubs/views/decorators/csp.pyi | 7 ++++++ scripts/stubtest/allowlist_todo_django60.txt | 6 ----- 6 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 django-stubs/middleware/csp.pyi create mode 100644 django-stubs/utils/csp.pyi create mode 100644 django-stubs/views/decorators/csp.pyi diff --git a/django-stubs/conf/global_settings.pyi b/django-stubs/conf/global_settings.pyi index 40367682c..b2d9d788b 100644 --- a/django-stubs/conf/global_settings.pyi +++ b/django-stubs/conf/global_settings.pyi @@ -541,3 +541,9 @@ SECURE_REDIRECT_EXEMPT: list[str] SECURE_REFERRER_POLICY: str SECURE_SSL_HOST: str | None SECURE_SSL_REDIRECT: bool + +################## +# CSP MIDDLEWARE # +################## +SECURE_CSP: dict[str, Any] = {} +SECURE_CSP_REPORT_ONLY: dict[str, Any] = {} diff --git a/django-stubs/middleware/csp.pyi b/django-stubs/middleware/csp.pyi new file mode 100644 index 000000000..8a89be706 --- /dev/null +++ b/django-stubs/middleware/csp.pyi @@ -0,0 +1,7 @@ +from django.http import HttpRequest, HttpResponse +from django.utils.csp import CSP as CSP +from django.utils.deprecation import MiddlewareMixin + +class ContentSecurityPolicyMiddleware(MiddlewareMixin): + def process_request(self, request: HttpRequest) -> None: ... + def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: ... diff --git a/django-stubs/template/context_processors.pyi b/django-stubs/template/context_processors.pyi index 15020b8c8..d7ec44979 100644 --- a/django-stubs/template/context_processors.pyi +++ b/django-stubs/template/context_processors.pyi @@ -6,6 +6,7 @@ from django.utils.functional import SimpleLazyObject _R = TypeVar("_R", bound=HttpRequest) +def csp(request: HttpRequest) -> dict[str, SimpleLazyObject | None]: ... def csrf(request: HttpRequest) -> dict[str, SimpleLazyObject]: ... def debug(request: HttpRequest) -> dict[str, Callable | bool]: ... def i18n(request: HttpRequest) -> dict[str, list[tuple[str, str]] | bool | str]: ... diff --git a/django-stubs/utils/csp.pyi b/django-stubs/utils/csp.pyi new file mode 100644 index 000000000..c23480753 --- /dev/null +++ b/django-stubs/utils/csp.pyi @@ -0,0 +1,24 @@ +import sys + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from enum import Enum + + class ReprEnum(Enum): ... # type: ignore[misc] + class StrEnum(str, ReprEnum): ... # type: ignore[misc] + +class CSP(StrEnum): + HEADER_ENFORCE = "Content-Security-Policy" + HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only" + + NONE = "'none'" + REPORT_SAMPLE = "'report-sample'" + SELF = "'self'" + STRICT_DYNAMIC = "'strict-dynamic'" + UNSAFE_EVAL = "'unsafe-eval'" + UNSAFE_HASHES = "'unsafe-hashes'" + UNSAFE_INLINE = "'unsafe-inline'" + WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" + + NONCE = "" diff --git a/django-stubs/views/decorators/csp.pyi b/django-stubs/views/decorators/csp.pyi new file mode 100644 index 000000000..923a3f374 --- /dev/null +++ b/django-stubs/views/decorators/csp.pyi @@ -0,0 +1,7 @@ +from collections.abc import Callable +from typing import Any, TypeVar + +_F = TypeVar("_F", bound=Callable[..., Any]) + +def csp_override(config: dict[str, Any]) -> Callable[[_F], _F]: ... +def csp_report_only_override(config: dict[str, Any]) -> Callable[[_F], _F]: ... diff --git a/scripts/stubtest/allowlist_todo_django60.txt b/scripts/stubtest/allowlist_todo_django60.txt index bbd6cc4e1..456626afd 100644 --- a/scripts/stubtest/allowlist_todo_django60.txt +++ b/scripts/stubtest/allowlist_todo_django60.txt @@ -1,7 +1,5 @@ django.conf.FORMS_URLFIELD_ASSUME_HTTPS_DEPRECATED_MSG django.conf.global_settings.FORMS_URLFIELD_ASSUME_HTTPS -django.conf.global_settings.SECURE_CSP -django.conf.global_settings.SECURE_CSP_REPORT_ONLY django.conf.global_settings.TASKS django.conf.global_settings.URLIZE_ASSUME_HTTPS django.contrib.admin.AdminSite.password_change_form @@ -212,7 +210,6 @@ django.forms.ClearableFileInput.use_fieldset django.forms.models.BaseModelForm.validate_constraints django.forms.renderers.Jinja2DivFormRenderer django.forms.widgets.ClearableFileInput.use_fieldset -django.middleware.csp django.tasks django.tasks.backends django.tasks.backends.base @@ -223,7 +220,6 @@ django.tasks.checks django.tasks.exceptions django.tasks.signals django.template.base.PartialTemplate -django.template.context_processors.csp django.template.defaulttags.PartialDefNode django.template.defaulttags.PartialNode django.template.defaulttags.partial_func @@ -232,7 +228,6 @@ django.test.runner.QueryFormatter django.test.selenium.SeleniumTestCase.get_browser_logs django.test.testcases._AssertTemplateUsedContext.rendered_template_names django.utils.copy -django.utils.csp django.utils.datastructures.DeferredSubDict django.utils.deprecation.RemovedInDjango60Warning django.utils.deprecation.RemovedInDjango70Warning @@ -245,4 +240,3 @@ django.utils.itercompat django.utils.json django.utils.log.log_message django.utils.text.acompress_sequence -django.views.decorators.csp From 00b8f29eda07760ef10feb5d6c61e69dca19fa8b Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 12 Dec 2025 11:35:11 +1100 Subject: [PATCH 2/3] Address review comments --- django-stubs/conf/global_settings.pyi | 4 ++-- django-stubs/middleware/csp.pyi | 3 +++ django-stubs/utils/csp.pyi | 17 ++++++++++---- django-stubs/views/decorators/csp.pyi | 6 ++--- tests/assert_type/views/test_decorators.py | 27 ++++++++++++++++++++++ 5 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 tests/assert_type/views/test_decorators.py diff --git a/django-stubs/conf/global_settings.pyi b/django-stubs/conf/global_settings.pyi index b2d9d788b..2085d6ce4 100644 --- a/django-stubs/conf/global_settings.pyi +++ b/django-stubs/conf/global_settings.pyi @@ -545,5 +545,5 @@ SECURE_SSL_REDIRECT: bool ################## # CSP MIDDLEWARE # ################## -SECURE_CSP: dict[str, Any] = {} -SECURE_CSP_REPORT_ONLY: dict[str, Any] = {} +SECURE_CSP: dict[str, Sequence[str] | str] +SECURE_CSP_REPORT_ONLY: dict[str, Sequence[str] | str] diff --git a/django-stubs/middleware/csp.pyi b/django-stubs/middleware/csp.pyi index 8a89be706..53efcc6e3 100644 --- a/django-stubs/middleware/csp.pyi +++ b/django-stubs/middleware/csp.pyi @@ -1,7 +1,10 @@ from django.http import HttpRequest, HttpResponse from django.utils.csp import CSP as CSP +from django.utils.csp import LazyNonce from django.utils.deprecation import MiddlewareMixin +def get_nonce(request: HttpRequest) -> LazyNonce | None: ... + class ContentSecurityPolicyMiddleware(MiddlewareMixin): def process_request(self, request: HttpRequest) -> None: ... def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: ... diff --git a/django-stubs/utils/csp.pyi b/django-stubs/utils/csp.pyi index c23480753..51863ef91 100644 --- a/django-stubs/utils/csp.pyi +++ b/django-stubs/utils/csp.pyi @@ -1,14 +1,17 @@ import sys +from collections.abc import Sequence + +from django.utils.functional import SimpleLazyObject if sys.version_info >= (3, 11): - from enum import StrEnum + from enum import StrEnum as _StrEnum else: from enum import Enum - class ReprEnum(Enum): ... # type: ignore[misc] - class StrEnum(str, ReprEnum): ... # type: ignore[misc] + class _ReprEnum(Enum): ... # type: ignore[misc] + class _StrEnum(str, _ReprEnum): ... # type: ignore[misc] -class CSP(StrEnum): +class CSP(_StrEnum): HEADER_ENFORCE = "Content-Security-Policy" HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only" @@ -22,3 +25,9 @@ class CSP(StrEnum): WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" NONCE = "" + +class LazyNonce(SimpleLazyObject): + def __init__(self) -> None: ... + def __bool__(self) -> bool: ... + +def build_policy(config: dict[str, Sequence[str] | str], nonce: SimpleLazyObject | str | None = None) -> str: ... diff --git a/django-stubs/views/decorators/csp.pyi b/django-stubs/views/decorators/csp.pyi index 923a3f374..2a9a68ec5 100644 --- a/django-stubs/views/decorators/csp.pyi +++ b/django-stubs/views/decorators/csp.pyi @@ -1,7 +1,7 @@ -from collections.abc import Callable +from collections.abc import Callable, Sequence from typing import Any, TypeVar _F = TypeVar("_F", bound=Callable[..., Any]) -def csp_override(config: dict[str, Any]) -> Callable[[_F], _F]: ... -def csp_report_only_override(config: dict[str, Any]) -> Callable[[_F], _F]: ... +def csp_override(config: dict[str, Sequence[str] | str]) -> Callable[[_F], _F]: ... +def csp_report_only_override(config: dict[str, Sequence[str] | str]) -> Callable[[_F], _F]: ... diff --git a/tests/assert_type/views/test_decorators.py b/tests/assert_type/views/test_decorators.py new file mode 100644 index 000000000..2e4ff32a7 --- /dev/null +++ b/tests/assert_type/views/test_decorators.py @@ -0,0 +1,27 @@ +from django.http import HttpRequest, HttpResponse +from django.views.decorators.csp import csp_override, csp_report_only_override +from typing_extensions import assert_type + + +@csp_override( + { + "default-src": ["'self'"], + "script-src": ["'self'", "'unsafe-inline'"], + "report-uri": "/path/to/reports-endpoint/", + } +) +def my_view(request: HttpRequest) -> HttpResponse: ... + + +@csp_report_only_override( + { + "default-src": ["'self'"], + "script-src": ["'self'", "'unsafe-inline'"], + "report-uri": "/path/to/reports-endpoint/", + } +) +def my_view2(request: HttpRequest) -> HttpResponse: ... + + +assert_type(my_view(HttpRequest()), HttpResponse) +assert_type(my_view2(HttpRequest()), HttpResponse) From 47e021e16a91bb961b22ad64e0168cdb79a963df Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 12 Dec 2025 12:59:35 +1100 Subject: [PATCH 3/3] Update types for CSP policies based on docs --- django-stubs/conf/global_settings.pyi | 6 +++--- django-stubs/utils/csp.pyi | 4 ++-- django-stubs/views/decorators/csp.pyi | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/django-stubs/conf/global_settings.pyi b/django-stubs/conf/global_settings.pyi index 2085d6ce4..d46010a8b 100644 --- a/django-stubs/conf/global_settings.pyi +++ b/django-stubs/conf/global_settings.pyi @@ -1,4 +1,4 @@ -from collections.abc import Sequence +from collections.abc import Collection, Mapping, Sequence from re import Pattern # This is defined here as a do-nothing function because we can't import @@ -545,5 +545,5 @@ SECURE_SSL_REDIRECT: bool ################## # CSP MIDDLEWARE # ################## -SECURE_CSP: dict[str, Sequence[str] | str] -SECURE_CSP_REPORT_ONLY: dict[str, Sequence[str] | str] +SECURE_CSP: Mapping[str, Collection[str] | str] +SECURE_CSP_REPORT_ONLY: Mapping[str, Collection[str] | str] diff --git a/django-stubs/utils/csp.pyi b/django-stubs/utils/csp.pyi index 51863ef91..c07366a88 100644 --- a/django-stubs/utils/csp.pyi +++ b/django-stubs/utils/csp.pyi @@ -1,5 +1,5 @@ import sys -from collections.abc import Sequence +from collections.abc import Collection, Mapping from django.utils.functional import SimpleLazyObject @@ -30,4 +30,4 @@ class LazyNonce(SimpleLazyObject): def __init__(self) -> None: ... def __bool__(self) -> bool: ... -def build_policy(config: dict[str, Sequence[str] | str], nonce: SimpleLazyObject | str | None = None) -> str: ... +def build_policy(config: Mapping[str, Collection[str] | str], nonce: SimpleLazyObject | str | None = None) -> str: ... diff --git a/django-stubs/views/decorators/csp.pyi b/django-stubs/views/decorators/csp.pyi index 2a9a68ec5..e2ddff3aa 100644 --- a/django-stubs/views/decorators/csp.pyi +++ b/django-stubs/views/decorators/csp.pyi @@ -1,7 +1,7 @@ -from collections.abc import Callable, Sequence +from collections.abc import Callable, Collection, Mapping from typing import Any, TypeVar _F = TypeVar("_F", bound=Callable[..., Any]) -def csp_override(config: dict[str, Sequence[str] | str]) -> Callable[[_F], _F]: ... -def csp_report_only_override(config: dict[str, Sequence[str] | str]) -> Callable[[_F], _F]: ... +def csp_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ... +def csp_report_only_override(config: Mapping[str, Collection[str] | str]) -> Callable[[_F], _F]: ...