mirror of
https://github.com/django/django.git
synced 2025-06-30 07:49:19 +00:00
Fixed #15727 -- Added Content Security Policy (CSP) support.
This initial work adds a pair of settings to configure specific CSP directives for enforcing or reporting policy violations, a new `django.middleware.csp.ContentSecurityPolicyMiddleware` to apply the appropriate headers to responses, and a context processor to support CSP nonces in templates for safely inlining assets. Relevant documentation has been added for the 6.0 release notes, security overview, a new how-to page, and a dedicated reference section. Thanks to the multiple reviewers for their precise and valuable feedback. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
parent
3f59711581
commit
d63241ebc7
@ -663,6 +663,12 @@ SECURE_REFERRER_POLICY = "same-origin"
|
|||||||
SECURE_SSL_HOST = None
|
SECURE_SSL_HOST = None
|
||||||
SECURE_SSL_REDIRECT = False
|
SECURE_SSL_REDIRECT = False
|
||||||
|
|
||||||
|
##################
|
||||||
|
# CSP MIDDLEWARE #
|
||||||
|
##################
|
||||||
|
SECURE_CSP = {}
|
||||||
|
SECURE_CSP_REPORT_ONLY = {}
|
||||||
|
|
||||||
# RemovedInDjango70Warning: A transitional setting helpful in early adoption of
|
# RemovedInDjango70Warning: A transitional setting helpful in early adoption of
|
||||||
# HTTPS as the default protocol in urlize and urlizetrunc when no protocol is
|
# HTTPS as the default protocol in urlize and urlizetrunc when no protocol is
|
||||||
# provided. Set to True to assume HTTPS during the Django 6.x release cycle.
|
# provided. Set to True to assume HTTPS during the Django 6.x release cycle.
|
||||||
|
@ -141,6 +141,11 @@ E024 = Error(
|
|||||||
|
|
||||||
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
|
||||||
|
|
||||||
|
E026 = Error(
|
||||||
|
"The Content Security Policy setting '%s' must be a dictionary (got %r instead).",
|
||||||
|
id="security.E026",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _security_middleware():
|
def _security_middleware():
|
||||||
return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
|
return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
|
||||||
@ -281,3 +286,19 @@ def check_cross_origin_opener_policy(app_configs, **kwargs):
|
|||||||
):
|
):
|
||||||
return [E024]
|
return [E024]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@register(Tags.security)
|
||||||
|
def check_csp_settings(app_configs, **kwargs):
|
||||||
|
"""
|
||||||
|
Validate that CSP settings are properly configured when enabled.
|
||||||
|
|
||||||
|
Ensures both SECURE_CSP and SECURE_CSP_REPORT_ONLY are dictionaries.
|
||||||
|
"""
|
||||||
|
# CSP settings must be a dictionary or None.
|
||||||
|
return [
|
||||||
|
Error(E026.msg % (name, value), id=E026.id)
|
||||||
|
for name in ("SECURE_CSP", "SECURE_CSP_REPORT_ONLY")
|
||||||
|
if (value := getattr(settings, name, None)) is not None
|
||||||
|
and not isinstance(value, dict)
|
||||||
|
]
|
||||||
|
36
django/middleware/csp.py
Normal file
36
django/middleware/csp.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.csp import CSP, LazyNonce, build_policy
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
|
||||||
|
|
||||||
|
def get_nonce(request):
|
||||||
|
return getattr(request, "_csp_nonce", None)
|
||||||
|
|
||||||
|
|
||||||
|
class ContentSecurityPolicyMiddleware(MiddlewareMixin):
|
||||||
|
def process_request(self, request):
|
||||||
|
request._csp_nonce = LazyNonce()
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
|
# In DEBUG mode, exclude CSP headers for specific status codes that
|
||||||
|
# trigger the debug view.
|
||||||
|
exempted_status_codes = {
|
||||||
|
HTTPStatus.NOT_FOUND,
|
||||||
|
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
if settings.DEBUG and response.status_code in exempted_status_codes:
|
||||||
|
return response
|
||||||
|
|
||||||
|
nonce = get_nonce(request)
|
||||||
|
for header, config in [
|
||||||
|
(CSP.HEADER_ENFORCE, settings.SECURE_CSP),
|
||||||
|
(CSP.HEADER_REPORT_ONLY, settings.SECURE_CSP_REPORT_ONLY),
|
||||||
|
]:
|
||||||
|
# If headers are already set on the response, don't overwrite them.
|
||||||
|
# This allows for views to set their own CSP headers as needed.
|
||||||
|
if config and header not in response:
|
||||||
|
response.headers[str(header)] = build_policy(config, nonce)
|
||||||
|
|
||||||
|
return response
|
@ -10,6 +10,7 @@ of a DjangoTemplates backend and used by RequestContext.
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.middleware.csp import get_nonce
|
||||||
from django.middleware.csrf import get_token
|
from django.middleware.csrf import get_token
|
||||||
from django.utils.functional import SimpleLazyObject, lazy
|
from django.utils.functional import SimpleLazyObject, lazy
|
||||||
|
|
||||||
@ -87,3 +88,10 @@ def media(request):
|
|||||||
|
|
||||||
def request(request):
|
def request(request):
|
||||||
return {"request": request}
|
return {"request": request}
|
||||||
|
|
||||||
|
|
||||||
|
def csp(request):
|
||||||
|
"""
|
||||||
|
Add the CSP nonce to the context.
|
||||||
|
"""
|
||||||
|
return {"csp_nonce": get_nonce(request)}
|
||||||
|
110
django/utils/csp.py
Normal file
110
django/utils/csp.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import secrets
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from django.utils.functional import SimpleLazyObject, empty
|
||||||
|
|
||||||
|
|
||||||
|
class CSP(StrEnum):
|
||||||
|
"""
|
||||||
|
Content Security Policy constants for directive values and special tokens.
|
||||||
|
|
||||||
|
These constants represent:
|
||||||
|
1. Standard quoted string values from the CSP spec (e.g., 'self', 'unsafe-inline')
|
||||||
|
2. Special placeholder tokens (NONCE) that get replaced by the middleware
|
||||||
|
|
||||||
|
Using this enum instead of raw strings provides better type checking,
|
||||||
|
autocompletion, and protection against common mistakes like:
|
||||||
|
|
||||||
|
- Typos (e.g., 'noone' instead of 'none')
|
||||||
|
- Missing quotes (e.g., ["self"] instead of ["'self'"])
|
||||||
|
- Inconsistent quote styles (e.g., ["'self'", "\"unsafe-inline\""])
|
||||||
|
|
||||||
|
Example usage in Django settings:
|
||||||
|
|
||||||
|
SECURE_CSP = {
|
||||||
|
"default-src": [CSP.NONE],
|
||||||
|
"script-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# HTTP Headers.
|
||||||
|
HEADER_ENFORCE = "Content-Security-Policy"
|
||||||
|
HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"
|
||||||
|
|
||||||
|
# Standard CSP directive values.
|
||||||
|
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'"
|
||||||
|
|
||||||
|
# Special placeholder that gets replaced by the middleware.
|
||||||
|
# The value itself is arbitrary and should not be mistaken for a real nonce.
|
||||||
|
NONCE = "<CSP_NONCE_SENTINEL>"
|
||||||
|
|
||||||
|
|
||||||
|
class LazyNonce(SimpleLazyObject):
|
||||||
|
"""
|
||||||
|
Lazily generates a cryptographically secure nonce string, for use in CSP headers.
|
||||||
|
|
||||||
|
The nonce is only generated when first accessed (e.g., via string
|
||||||
|
interpolation or inside a template).
|
||||||
|
|
||||||
|
The nonce will evaluate as `True` if it has been generated, and `False` if
|
||||||
|
it has not. This is useful for third-party Django libraries that want to
|
||||||
|
support CSP without requiring it.
|
||||||
|
|
||||||
|
Example Django template usage with context processors enabled:
|
||||||
|
|
||||||
|
<script{% if csp_nonce %} nonce="{{ csp_nonce }}"...{% endif %}>
|
||||||
|
|
||||||
|
The `{% if %}` block will only render if the nonce has been evaluated elsewhere.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(self._generate)
|
||||||
|
|
||||||
|
def _generate(self):
|
||||||
|
return secrets.token_urlsafe(16)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return self._wrapped is not empty
|
||||||
|
|
||||||
|
|
||||||
|
def build_policy(config, nonce=None):
|
||||||
|
policy = []
|
||||||
|
|
||||||
|
for directive, values in config.items():
|
||||||
|
if values in (None, False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if values is True:
|
||||||
|
rendered_value = ""
|
||||||
|
else:
|
||||||
|
if isinstance(values, set):
|
||||||
|
# Sort values for consistency, preventing cache invalidation
|
||||||
|
# between requests and ensuring reliable browser caching.
|
||||||
|
values = sorted(values)
|
||||||
|
elif not isinstance(values, list | tuple):
|
||||||
|
values = [values]
|
||||||
|
|
||||||
|
# Replace the nonce sentinel with the actual nonce values, if the
|
||||||
|
# sentinel is found and a nonce is provided. Otherwise, remove it.
|
||||||
|
if (has_sentinel := CSP.NONCE in values) and nonce:
|
||||||
|
values = [f"'nonce-{nonce}'" if v == CSP.NONCE else v for v in values]
|
||||||
|
elif has_sentinel:
|
||||||
|
values = [v for v in values if v != CSP.NONCE]
|
||||||
|
|
||||||
|
if not values:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rendered_value = " ".join(values)
|
||||||
|
|
||||||
|
policy.append(f"{directive} {rendered_value}".rstrip())
|
||||||
|
|
||||||
|
return "; ".join(policy)
|
100
docs/howto/csp.txt
Normal file
100
docs/howto/csp.txt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
===========================================
|
||||||
|
How to use Django's Content Security Policy
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
.. _csp-config:
|
||||||
|
|
||||||
|
Basic config
|
||||||
|
============
|
||||||
|
|
||||||
|
To enable Content Security Policy (CSP) in your Django project:
|
||||||
|
|
||||||
|
1. Add the CSP middleware to your :setting:`MIDDLEWARE` setting::
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
# ...
|
||||||
|
"django.middleware.csp.ContentSecurityPolicyMiddleware",
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
2. Configure the CSP policies in your ``settings.py`` using either
|
||||||
|
:setting:`SECURE_CSP` or :setting:`SECURE_CSP_REPORT_ONLY` (or both). The
|
||||||
|
:ref:`CSP Settings docs <csp-settings>` provide more details about the
|
||||||
|
differences between these two::
|
||||||
|
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
# To enforce a CSP policy:
|
||||||
|
SECURE_CSP = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
# Add more directives to be enforced.
|
||||||
|
}
|
||||||
|
|
||||||
|
# Or for report-only mode:
|
||||||
|
SECURE_CSP_REPORT_ONLY = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
# Add more directives as needed.
|
||||||
|
"report-uri": "/path/to/reports-endpoint/",
|
||||||
|
}
|
||||||
|
|
||||||
|
.. _csp-nonce-config:
|
||||||
|
|
||||||
|
Nonce config
|
||||||
|
============
|
||||||
|
|
||||||
|
To use nonces in your CSP policy, beside the basic config, you need to:
|
||||||
|
|
||||||
|
1. Include the :attr:`~django.utils.csp.CSP.NONCE` placeholder value in the CSP
|
||||||
|
settings. This only applies to ``script-src`` or ``style-src`` directives::
|
||||||
|
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
SECURE_CSP = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
# Allow self-hosted scripts and script tags with matching `nonce` attr.
|
||||||
|
"script-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
# Example of the less secure 'unsafe-inline' option.
|
||||||
|
"style-src": [CSP.SELF, CSP.UNSAFE_INLINE],
|
||||||
|
}
|
||||||
|
|
||||||
|
2. Add the :func:`~django.template.context_processors.csp` context processor to
|
||||||
|
your :setting:`TEMPLATES` setting. This makes the generated nonce value
|
||||||
|
available in the Django templates as the ``csp_nonce`` context variable::
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
# ...
|
||||||
|
"django.template.context_processors.csp",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
3. In your templates, add the ``nonce`` attribute to the relevant inline
|
||||||
|
``<style>`` or ``<script>`` tags, using the ``csp_nonce`` context variable:
|
||||||
|
|
||||||
|
.. code-block:: html+django
|
||||||
|
|
||||||
|
<style nonce="{{ csp_nonce }}">
|
||||||
|
/* These inline styles will be allowed. */
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
// This inline JavaScript will be allowed.
|
||||||
|
</script>
|
||||||
|
|
||||||
|
.. admonition:: Caching and Nonce Reuse
|
||||||
|
|
||||||
|
The :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`
|
||||||
|
automatically handles generating a unique nonce and inserting the
|
||||||
|
appropriate ``nonce-<value>`` source expression into the
|
||||||
|
``Content-Security-Policy`` (or ``Content-Security-Policy-Report-Only``)
|
||||||
|
header when the nonce is used in a template.
|
||||||
|
|
||||||
|
To ensure correct behavior, make sure both the HTML and the header are
|
||||||
|
generated within the same request and not served from cache. See the
|
||||||
|
reference documentation on :ref:`csp-nonce` for implementation details and
|
||||||
|
important caching considerations.
|
@ -57,6 +57,7 @@ Other guides
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
auth-remote-user
|
auth-remote-user
|
||||||
|
csp
|
||||||
csrf
|
csrf
|
||||||
custom-file-storage
|
custom-file-storage
|
||||||
custom-management-commands
|
custom-management-commands
|
||||||
|
@ -251,6 +251,7 @@ applications and Django provides multiple protection tools and mechanisms:
|
|||||||
* :doc:`Cross Site Request Forgery protection <ref/csrf>`
|
* :doc:`Cross Site Request Forgery protection <ref/csrf>`
|
||||||
* :doc:`Cryptographic signing <topics/signing>`
|
* :doc:`Cryptographic signing <topics/signing>`
|
||||||
* :ref:`Security Middleware <security-middleware>`
|
* :ref:`Security Middleware <security-middleware>`
|
||||||
|
* :doc:`Content Security Policy <ref/csp>`
|
||||||
|
|
||||||
Internationalization and localization
|
Internationalization and localization
|
||||||
=====================================
|
=====================================
|
||||||
|
@ -568,6 +568,8 @@ The following checks are run if you use the :option:`check --deploy` option:
|
|||||||
``'django-insecure-'`` indicating that it was generated automatically by
|
``'django-insecure-'`` indicating that it was generated automatically by
|
||||||
Django. Please generate a long and random value, otherwise many of Django's
|
Django. Please generate a long and random value, otherwise many of Django's
|
||||||
security-critical features will be vulnerable to attack.
|
security-critical features will be vulnerable to attack.
|
||||||
|
* **security.E026**: The CSP setting ``<SETTING_NAME>`` must be a dictionary
|
||||||
|
(got ``<value>`` instead).
|
||||||
|
|
||||||
The following checks verify that your security-related settings are correctly
|
The following checks verify that your security-related settings are correctly
|
||||||
configured:
|
configured:
|
||||||
|
210
docs/ref/csp.txt
Normal file
210
docs/ref/csp.txt
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
=======================
|
||||||
|
Content Security Policy
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
.. module:: django.middleware.csp
|
||||||
|
:synopsis: Middleware for Content Security Policy headers
|
||||||
|
|
||||||
|
Content Security Policy (CSP) is a web security standard that helps prevent
|
||||||
|
content injection attacks by restricting the sources from which content can be
|
||||||
|
loaded. It plays an important role in a comprehensive :ref:`security strategy
|
||||||
|
<security-csp>`.
|
||||||
|
|
||||||
|
For configuration instructions in a Django project, see the :ref:`Using CSP
|
||||||
|
<csp-config>` documentation. For an HTTP guide about CSP, see the `MDN Guide on
|
||||||
|
CSP <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP>`_.
|
||||||
|
|
||||||
|
.. _csp-overview:
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
The `Content-Security-Policy specification <https://www.w3.org/TR/CSP3/>`_
|
||||||
|
defines two complementary headers:
|
||||||
|
|
||||||
|
* ``Content-Security-Policy``: Enforces the CSP policy, blocking content that
|
||||||
|
violates the defined directives.
|
||||||
|
* ``Content-Security-Policy-Report-Only``: Reports CSP violations without
|
||||||
|
blocking content, allowing for non-intrusive testing.
|
||||||
|
|
||||||
|
Each policy is composed of one or more directives and their values, which
|
||||||
|
together instruct the browser on how to handle specific types of content.
|
||||||
|
|
||||||
|
When the :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` is
|
||||||
|
enabled, Django automatically builds and attaches the appropriate headers to
|
||||||
|
each response based on the configured :ref:`settings <csp-settings>`, unless
|
||||||
|
they have already been set by another layer.
|
||||||
|
|
||||||
|
.. _csp-settings:
|
||||||
|
|
||||||
|
Settings
|
||||||
|
========
|
||||||
|
|
||||||
|
The :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` is
|
||||||
|
configured using the following settings:
|
||||||
|
|
||||||
|
* :setting:`SECURE_CSP`: defines the **enforced Content Security Policy**.
|
||||||
|
* :setting:`SECURE_CSP_REPORT_ONLY`: defines a **report-only Content Security Policy**.
|
||||||
|
|
||||||
|
.. admonition:: These settings can be used independently or together
|
||||||
|
|
||||||
|
* Use :setting:`SECURE_CSP` alone to enforce a policy that has already been
|
||||||
|
tested and verified.
|
||||||
|
* Use :setting:`SECURE_CSP_REPORT_ONLY` on its own to evaluate a new policy
|
||||||
|
without disrupting site behavior. This mode does not block violations, it
|
||||||
|
only logs them. It's useful for testing and monitoring, but provides no
|
||||||
|
protection against active threats.
|
||||||
|
* Use *both* to maintain an enforced baseline while experimenting with
|
||||||
|
changes. Even for well-established policies, continuing to collect reports
|
||||||
|
reports can help detect regressions, unexpected changes in behavior, or
|
||||||
|
potential tampering in production environments.
|
||||||
|
|
||||||
|
.. _csp-reports:
|
||||||
|
|
||||||
|
Policy violation reports
|
||||||
|
========================
|
||||||
|
|
||||||
|
When a CSP violation occurs, browsers typically log details to the developer
|
||||||
|
console, providing immediate feedback during development. To also receive these
|
||||||
|
reports programmatically, the policy must include a `reporting directive
|
||||||
|
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy#reporting_directives>`_
|
||||||
|
such as ``report-uri`` that specifies where violation data should be sent.
|
||||||
|
|
||||||
|
Django supports configuring these directives via the
|
||||||
|
:setting:`SECURE_CSP_REPORT_ONLY` settings, but reports will only be issued by
|
||||||
|
the browser if the policy explicitly includes a valid reporting directive.
|
||||||
|
|
||||||
|
Django does not provide built-in functionality to receive, store, or process
|
||||||
|
violation reports. To collect and analyze them, you must implement your own
|
||||||
|
reporting endpoint or integrate with a third-party monitoring service.
|
||||||
|
|
||||||
|
.. _csp-constants:
|
||||||
|
|
||||||
|
CSP constants
|
||||||
|
=============
|
||||||
|
|
||||||
|
Django provides predefined constants representing common CSP source expression
|
||||||
|
keywords such as ``'self'``, ``'none'``, and ``'unsafe-inline'``. These
|
||||||
|
constants are intended for use in the directive values defined in the settings.
|
||||||
|
|
||||||
|
They are available through the :class:`~django.utils.csp.CSP` enum, and using
|
||||||
|
them is recommended over raw strings. This helps avoid common mistakes such as
|
||||||
|
typos, improper quoting, or inconsistent formatting, and ensures compliance
|
||||||
|
with the CSP specification.
|
||||||
|
|
||||||
|
.. module:: django.utils.csp
|
||||||
|
:synopsis: Constants for Content Security Policy
|
||||||
|
|
||||||
|
.. class:: CSP
|
||||||
|
|
||||||
|
Enum providing standardized constants for common CSP source expressions.
|
||||||
|
|
||||||
|
.. attribute:: NONE
|
||||||
|
|
||||||
|
Represents ``'none'``. Blocks loading resources for the given directive.
|
||||||
|
|
||||||
|
.. attribute:: REPORT_SAMPLE
|
||||||
|
|
||||||
|
Represents ``'report-sample'``. Instructs the browser to include a sample
|
||||||
|
of the violating code in reports. Note that this may expose sensitive
|
||||||
|
data.
|
||||||
|
|
||||||
|
.. attribute:: SELF
|
||||||
|
|
||||||
|
Represents ``'self'``. Allows loading resources from the same origin
|
||||||
|
(same scheme, host, and port).
|
||||||
|
|
||||||
|
.. attribute:: STRICT_DYNAMIC
|
||||||
|
|
||||||
|
Represents ``'strict-dynamic'``. Allows execution of scripts loaded by a
|
||||||
|
trusted script (e.g., one with a valid nonce or hash), without needing
|
||||||
|
``'unsafe-inline'``.
|
||||||
|
|
||||||
|
.. attribute:: UNSAFE_EVAL
|
||||||
|
|
||||||
|
Represents ``'unsafe-eval'``. Allows use of ``eval()`` and similar
|
||||||
|
JavaScript functions. Strongly discouraged.
|
||||||
|
|
||||||
|
.. attribute:: UNSAFE_HASHES
|
||||||
|
|
||||||
|
Represents ``'unsafe-hashes'``. Allows inline event handlers and some
|
||||||
|
``javascript:`` URIs when their content hashes match a policy rule.
|
||||||
|
Requires CSP Level 3+.
|
||||||
|
|
||||||
|
.. attribute:: UNSAFE_INLINE
|
||||||
|
|
||||||
|
Represents ``'unsafe-inline'``. Allows execution of inline scripts,
|
||||||
|
styles, and ``javascript:`` URLs. Generally discouraged, especially for
|
||||||
|
scripts.
|
||||||
|
|
||||||
|
.. attribute:: WASM_UNSAFE_EVAL
|
||||||
|
|
||||||
|
Represents ``'wasm-unsafe-eval'``. Permits compilation and execution of
|
||||||
|
WebAssembly code without enabling ``'unsafe-eval'`` for scripts.
|
||||||
|
|
||||||
|
.. attribute:: NONCE
|
||||||
|
|
||||||
|
Django-specific placeholder value (``"<CSP_NONCE_SENTINEL>"``) used in
|
||||||
|
``script-src`` or ``style-src`` directives to activate nonce-based CSP.
|
||||||
|
This string is replaced at runtime by the
|
||||||
|
:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` with a
|
||||||
|
secure, random nonce that is generated for each request. See detailed
|
||||||
|
explanation in :ref:`csp-nonce`.
|
||||||
|
|
||||||
|
.. _csp-nonce:
|
||||||
|
|
||||||
|
Nonce usage
|
||||||
|
===========
|
||||||
|
|
||||||
|
A CSP nonce ("number used once") is a unique, random value generated per HTTP
|
||||||
|
response. Django supports nonces as a secure way to allow specific inline
|
||||||
|
``<script>`` or ``<style>`` elements to execute without relying on
|
||||||
|
``'unsafe-inline'``.
|
||||||
|
|
||||||
|
Nonces are enabled by including the special placeholder
|
||||||
|
:attr:`~django.utils.csp.CSP.NONCE` in the relevant directive(s) of your
|
||||||
|
:ref:`CSP settings <csp-settings>`, such as ``script-src`` or ``style-src``.
|
||||||
|
When present, the
|
||||||
|
:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`
|
||||||
|
will generate a nonce and insert the corresponding ``nonce-<value>`` source
|
||||||
|
expression into the CSP header.
|
||||||
|
|
||||||
|
To use this nonce in templates, the
|
||||||
|
:func:`~django.template.context_processors.csp` context processor needs to be
|
||||||
|
enabled. It adds a ``csp_nonce`` variable to the template context, allowing
|
||||||
|
inline elements to include a matching ``nonce={{ csp_nonce }}`` attribute in
|
||||||
|
inline scripts or styles.
|
||||||
|
|
||||||
|
The browser will only execute inline elements that include a ``nonce=<value>``
|
||||||
|
attribute matching the one specified in the ``Content-Security-Policy`` (or
|
||||||
|
``Content-Security-Policy-Report-Only``) header. This mechanism provides
|
||||||
|
fine-grained control over which inline code is allowed to run.
|
||||||
|
|
||||||
|
If a template includes ``{{ csp_nonce }}`` but the policy does not include
|
||||||
|
:attr:`~django.utils.csp.CSP.NONCE`, the HTML will include a nonce attribute,
|
||||||
|
but the header will lack the required source expression. In this case, the
|
||||||
|
browser will block the inline script or style (or report it for report-only
|
||||||
|
configurations).
|
||||||
|
|
||||||
|
Nonce generation and caching
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Django's nonce generation is **lazy**: the middleware only generates a nonce if
|
||||||
|
``{{ csp_nonce }}`` is accessed during template rendering. This avoids
|
||||||
|
unnecessary work for pages that do not use nonces.
|
||||||
|
|
||||||
|
However, because nonces must be unique per request, extra care is needed when
|
||||||
|
using full-page caching (e.g., Django's cache middleware, CDN caching). Serving
|
||||||
|
cached responses with previously generated nonces may result in reuse across
|
||||||
|
users and requests. Although such responses may still appear to work (since the
|
||||||
|
nonce in the CSP header and HTML content match), reuse defeats the purpose of
|
||||||
|
the nonce and weakens security.
|
||||||
|
|
||||||
|
To ensure nonce-based policies remain effective:
|
||||||
|
|
||||||
|
* Avoid caching full responses that include ``{{ csp_nonce }}``.
|
||||||
|
* If caching is necessary, use a strategy that injects a fresh nonce on each
|
||||||
|
request, or consider refactoring your application to avoid inline scripts and
|
||||||
|
styles altogether.
|
@ -10,6 +10,7 @@ API Reference
|
|||||||
class-based-views/index
|
class-based-views/index
|
||||||
clickjacking
|
clickjacking
|
||||||
contrib/index
|
contrib/index
|
||||||
|
csp
|
||||||
csrf
|
csrf
|
||||||
databases
|
databases
|
||||||
django-admin
|
django-admin
|
||||||
|
@ -607,6 +607,26 @@ You can add Cross Site Request Forgery protection to individual views using the
|
|||||||
|
|
||||||
Simple :doc:`clickjacking protection via the X-Frame-Options header </ref/clickjacking/>`.
|
Simple :doc:`clickjacking protection via the X-Frame-Options header </ref/clickjacking/>`.
|
||||||
|
|
||||||
|
Content Security Policy middleware
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. currentmodule:: django.middleware.csp
|
||||||
|
|
||||||
|
.. class:: ContentSecurityPolicyMiddleware
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
Adds support for Content Security Policy (CSP), which helps mitigate risks such
|
||||||
|
as Cross-Site Scripting (XSS) and data injection attacks by controlling the
|
||||||
|
sources of content that can be loaded in the browser. See the
|
||||||
|
:ref:`csp-overview` documentation for details on configuring policies.
|
||||||
|
|
||||||
|
This middleware sets the following headers on the response depending on the
|
||||||
|
available settings:
|
||||||
|
|
||||||
|
* ``Content-Security-Policy``, based on :setting:`SECURE_CSP`.
|
||||||
|
* ``Content-Security-Policy-Report-Only``, based on :setting:`SECURE_CSP_REPORT_ONLY`.
|
||||||
|
|
||||||
.. _middleware-ordering:
|
.. _middleware-ordering:
|
||||||
|
|
||||||
Middleware ordering
|
Middleware ordering
|
||||||
@ -691,6 +711,12 @@ Here are some hints about the ordering of various Django middleware classes:
|
|||||||
After any middleware that modifies the ``Vary`` header: that header is used
|
After any middleware that modifies the ``Vary`` header: that header is used
|
||||||
to pick a value for the cache hash-key.
|
to pick a value for the cache hash-key.
|
||||||
|
|
||||||
|
#. :class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`
|
||||||
|
|
||||||
|
Can be placed near the bottom, but ensure any middleware that accesses
|
||||||
|
:ref:`csp_nonce <csp-nonce>` is positioned after it, so the nonce is
|
||||||
|
properly included in the response header.
|
||||||
|
|
||||||
#. :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`
|
#. :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`
|
||||||
|
|
||||||
Should be near the bottom as it's a last-resort type of middleware.
|
Should be near the bottom as it's a last-resort type of middleware.
|
||||||
|
@ -2363,6 +2363,94 @@ Unless set to ``None``, the
|
|||||||
:ref:`cross-origin-opener-policy` header on all responses that do not already
|
:ref:`cross-origin-opener-policy` header on all responses that do not already
|
||||||
have it to the value provided.
|
have it to the value provided.
|
||||||
|
|
||||||
|
.. setting:: SECURE_CSP
|
||||||
|
|
||||||
|
``SECURE_CSP``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
Default: ``{}``
|
||||||
|
|
||||||
|
This setting defines the directives used by the
|
||||||
|
:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`, which
|
||||||
|
generates and adds a :ref:`Content-Security-Policy <csp-overview>` (CSP) header
|
||||||
|
to all responses that do not already include one.
|
||||||
|
|
||||||
|
The ``Content-Security-Policy`` header instructs browsers to restrict which
|
||||||
|
resources a page is allowed to load. A properly configured CSP can block
|
||||||
|
content that violates defined rules, helping prevent cross-site scripting (XSS)
|
||||||
|
and other content injection attacks by explicitly declaring trusted sources for
|
||||||
|
content such as scripts, styles, images, fonts, and more.
|
||||||
|
|
||||||
|
The setting must be a mapping (typically a dictionary) of directive names to
|
||||||
|
their values. Each key should be a valid CSP directive such as ``default-src``
|
||||||
|
or ``script-src``. The corresponding value can be a list, tuple, or set of
|
||||||
|
source expressions or URLs to allow for that directive. If a set is used, it
|
||||||
|
will be automatically sorted to ensure consistent output in the generated
|
||||||
|
headers.
|
||||||
|
|
||||||
|
This example illustrates the expected structure, using the constants defined in
|
||||||
|
:ref:`csp-constants`::
|
||||||
|
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
SECURE_CSP = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
"img-src": ["data:", CSP.SELF, "https://images.example.com"],
|
||||||
|
"frame-src": [CSP.NONE],
|
||||||
|
}
|
||||||
|
|
||||||
|
.. admonition:: Directives validation
|
||||||
|
|
||||||
|
Django's CSP middleware helps construct and send the appropriate header
|
||||||
|
based on your settings, but it does **not validate** that the directives and
|
||||||
|
values conform to the CSP specification. It is your responsibility to ensure
|
||||||
|
that the configuration is syntactically and semantically correct. Use
|
||||||
|
browser developer tools or external CSP validators during development.
|
||||||
|
|
||||||
|
For a list of available directives and their values, refer to the `MDN
|
||||||
|
documentation on CSP directives
|
||||||
|
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives>`_.
|
||||||
|
|
||||||
|
|
||||||
|
.. setting:: SECURE_CSP_REPORT_ONLY
|
||||||
|
|
||||||
|
``SECURE_CSP_REPORT_ONLY``
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
Default: ``{}``
|
||||||
|
|
||||||
|
This setting is just like :setting:`SECURE_CSP`, but instead of enforcing the
|
||||||
|
policy, it instructs the
|
||||||
|
:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware` to apply a
|
||||||
|
``Content-Security-Policy-Report-Only`` header to responses, which allows
|
||||||
|
browsers to monitor and report policy violations without blocking content. This
|
||||||
|
is useful for testing and refining a policy before enforcement.
|
||||||
|
|
||||||
|
Most browsers log CSP violations to the developer console and can optionally
|
||||||
|
send them to a reporting endpoint. To collect these reports, the ``report-uri``
|
||||||
|
directive must be defined (see :ref:`csp-reports` for more details).
|
||||||
|
|
||||||
|
As noted in the `MDN documentation on Content-Security-Policy-Report-Only
|
||||||
|
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only>`_,
|
||||||
|
the ``report-uri`` directive must be specified for reports to be sent;
|
||||||
|
otherwise, the header has no reporting effect (other than logging to the
|
||||||
|
browser's developer tools console).
|
||||||
|
|
||||||
|
Following the example from the :setting:`SECURE_CSP` setting::
|
||||||
|
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
SECURE_CSP_REPORT_ONLY = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
"img-src": ["data:", CSP.SELF, "https://images.example.com"],
|
||||||
|
"frame-src": [CSP.NONE],
|
||||||
|
"report-uri": "/my-site/csp/reports/",
|
||||||
|
}
|
||||||
|
|
||||||
.. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS
|
.. setting:: SECURE_HSTS_INCLUDE_SUBDOMAINS
|
||||||
|
|
||||||
``SECURE_HSTS_INCLUDE_SUBDOMAINS``
|
``SECURE_HSTS_INCLUDE_SUBDOMAINS``
|
||||||
@ -3749,6 +3837,8 @@ HTTP
|
|||||||
|
|
||||||
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
* :setting:`SECURE_CONTENT_TYPE_NOSNIFF`
|
||||||
* :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
|
* :setting:`SECURE_CROSS_ORIGIN_OPENER_POLICY`
|
||||||
|
* :setting:`SECURE_CSP`
|
||||||
|
* :setting:`SECURE_CSP_REPORT_ONLY`
|
||||||
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
* :setting:`SECURE_HSTS_INCLUDE_SUBDOMAINS`
|
||||||
* :setting:`SECURE_HSTS_PRELOAD`
|
* :setting:`SECURE_HSTS_PRELOAD`
|
||||||
* :setting:`SECURE_HSTS_SECONDS`
|
* :setting:`SECURE_HSTS_SECONDS`
|
||||||
|
@ -802,6 +802,18 @@ This processor adds a token that is needed by the :ttag:`csrf_token` template
|
|||||||
tag for protection against :doc:`Cross Site Request Forgeries
|
tag for protection against :doc:`Cross Site Request Forgeries
|
||||||
</ref/csrf>`.
|
</ref/csrf>`.
|
||||||
|
|
||||||
|
``django.template.context_processors.csp``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. function:: csp(request)
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
If this processor is enabled, every ``RequestContext`` will contain a variable
|
||||||
|
``csp_nonce``, providing a securely generated, request-specific nonce suitable
|
||||||
|
for use under a Content Security Policy. See :ref:`CSP nonce usage <csp-nonce>`
|
||||||
|
for details.
|
||||||
|
|
||||||
``django.template.context_processors.request``
|
``django.template.context_processors.request``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -37,6 +37,43 @@ compatible with Django 6.0.
|
|||||||
What's new in Django 6.0
|
What's new in Django 6.0
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
Content Security Policy support
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Built-in support for the :ref:`Content Security Policy (CSP) <security-csp>`
|
||||||
|
standard is now available, making it easier to protect web applications against
|
||||||
|
content injection attacks such as cross-site scripting (XSS). CSP allows
|
||||||
|
declaring trusted sources of content by giving browsers strict rules about
|
||||||
|
which scripts, styles, images, or other resources can be loaded.
|
||||||
|
|
||||||
|
CSP policies can now be enforced or monitored directly using built-in tools:
|
||||||
|
headers are added via the
|
||||||
|
:class:`~django.middleware.csp.ContentSecurityPolicyMiddleware`, nonces are
|
||||||
|
supported through the :func:`~django.template.context_processors.csp` context
|
||||||
|
processor, and policies are configured using the :setting:`SECURE_CSP` and
|
||||||
|
:setting:`SECURE_CSP_REPORT_ONLY` settings.
|
||||||
|
|
||||||
|
These settings accept Python dictionaries and support Django-provided constants
|
||||||
|
for clarity and safety. For example::
|
||||||
|
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
SECURE_CSP = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
"script-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
"img-src": [CSP.SELF, "https:"],
|
||||||
|
}
|
||||||
|
|
||||||
|
The resulting ``Content-Security-Policy`` header would be set to:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
default-src 'self'; script-src 'self' 'nonce-SECRET'; img-src 'self' https:
|
||||||
|
|
||||||
|
To get started, follow the :doc:`CSP how-to guide </howto/csp>`. For in-depth
|
||||||
|
guidance, see the :ref:`CSP security overview <security-csp>` and the
|
||||||
|
:doc:`reference docs </ref/csp>`.
|
||||||
|
|
||||||
Minor features
|
Minor features
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -310,6 +310,7 @@ needsinfo
|
|||||||
německy
|
německy
|
||||||
nginx
|
nginx
|
||||||
noding
|
noding
|
||||||
|
nonces
|
||||||
nonnegative
|
nonnegative
|
||||||
nullable
|
nullable
|
||||||
OAuth
|
OAuth
|
||||||
|
@ -286,6 +286,61 @@ User-uploaded content
|
|||||||
|
|
||||||
.. _same-origin policy: https://en.wikipedia.org/wiki/Same-origin_policy
|
.. _same-origin policy: https://en.wikipedia.org/wiki/Same-origin_policy
|
||||||
|
|
||||||
|
.. _security-csp:
|
||||||
|
|
||||||
|
Content Security Policy
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
Content Security Policy (CSP) is a browser security mechanism that helps
|
||||||
|
protect web applications against attacks such as cross-site scripting (XSS) and
|
||||||
|
other content injection attacks.
|
||||||
|
|
||||||
|
CSP allows web applications to define which sources of content are trusted,
|
||||||
|
instructing the browser to load, execute, or render resources only from those
|
||||||
|
sources. This effectively creates an allowlist of content origins, reducing the
|
||||||
|
risk of malicious code execution.
|
||||||
|
|
||||||
|
Key benefits of enabling CSP include:
|
||||||
|
|
||||||
|
1. Mitigating XSS attacks by blocking inline scripts and restricting external
|
||||||
|
script loading.
|
||||||
|
2. Controlling which external resources (e.g., images, fonts, stylesheets) can
|
||||||
|
be loaded.
|
||||||
|
3. Preventing unwanted framing of your site to protect against clickjacking.
|
||||||
|
4. Reporting violations to a specified endpoint, enabling monitoring and
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
For configuration instructions, see the :ref:`Using CSP <csp-config>`
|
||||||
|
documentation, and refer to the :ref:`CSP overview <csp-overview>` for details
|
||||||
|
on directives and settings.
|
||||||
|
|
||||||
|
Limitations and considerations
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
While CSP is a powerful security mechanism, it's important to understand its
|
||||||
|
limitations and implications, particularly when used in Django:
|
||||||
|
|
||||||
|
* Policy exclusion risks: Avoid excluding specific paths or responses from
|
||||||
|
CSP protection. Due to the browser’s same-origin policy, a vulnerability on
|
||||||
|
an unprotected page (e.g., one allowing arbitrary script injection) may be
|
||||||
|
leveraged to attack protected pages. Excluding *any* route can significantly
|
||||||
|
weaken the site's overall CSP protection.
|
||||||
|
|
||||||
|
* Performance overhead: Although typically negligible, CSP adds some processing
|
||||||
|
overhead. Nonce generation involves secure randomness for each applicable
|
||||||
|
request. For high-traffic applications or resource-constrained environments,
|
||||||
|
measure the performance impact accordingly.
|
||||||
|
|
||||||
|
* Browser support: While CSP Levels 1 and 2 are widely supported, newer
|
||||||
|
directives (CSP Level 3+) or complex policy behaviors may vary across
|
||||||
|
browsers. Test your policy across the environments you intend to support.
|
||||||
|
|
||||||
|
Despite these limitations, CSP remains an important and recommended security
|
||||||
|
layer for web applications. Understanding its constraints will help you design
|
||||||
|
a more effective and reliable deployment.
|
||||||
|
|
||||||
.. _additional-security-topics:
|
.. _additional-security-topics:
|
||||||
|
|
||||||
Additional security topics
|
Additional security topics
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import itertools
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.checks.messages import Error, Warning
|
from django.core.checks.messages import Error, Warning
|
||||||
from django.core.checks.security import base, csrf, sessions
|
from django.core.checks.security import base, csrf, sessions
|
||||||
@ -678,3 +680,54 @@ class CheckCrossOriginOpenerPolicyTest(SimpleTestCase):
|
|||||||
)
|
)
|
||||||
def test_with_invalid_coop(self):
|
def test_with_invalid_coop(self):
|
||||||
self.assertEqual(base.check_cross_origin_opener_policy(None), [base.E024])
|
self.assertEqual(base.check_cross_origin_opener_policy(None), [base.E024])
|
||||||
|
|
||||||
|
|
||||||
|
class CheckSecureCSPTests(SimpleTestCase):
|
||||||
|
"""Tests for the CSP settings check function."""
|
||||||
|
|
||||||
|
def test_secure_csp_allowed_values(self):
|
||||||
|
"""Check should pass when both CSP settings are None or dicts."""
|
||||||
|
allowed_values = (None, {}, {"key": "value"})
|
||||||
|
combinations = itertools.product(allowed_values, repeat=2)
|
||||||
|
for csp_value, csp_report_only_value in combinations:
|
||||||
|
with (
|
||||||
|
self.subTest(
|
||||||
|
csp_value=csp_value, csp_report_only_value=csp_report_only_value
|
||||||
|
),
|
||||||
|
self.settings(
|
||||||
|
SECURE_CSP=csp_value, SECURE_CSP_REPORT_ONLY=csp_report_only_value
|
||||||
|
),
|
||||||
|
):
|
||||||
|
errors = base.check_csp_settings(None)
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
|
def test_secure_csp_invalid_values(self):
|
||||||
|
"""Check should fail when either CSP setting is not a dict."""
|
||||||
|
for value in (
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
0,
|
||||||
|
42,
|
||||||
|
"",
|
||||||
|
"not-a-dict",
|
||||||
|
set(),
|
||||||
|
{"a", "b"},
|
||||||
|
[],
|
||||||
|
[1, 2, 3, 4],
|
||||||
|
):
|
||||||
|
with self.subTest(value=value):
|
||||||
|
csp_error = Error(
|
||||||
|
base.E026.msg % ("SECURE_CSP", value), id=base.E026.id
|
||||||
|
)
|
||||||
|
with self.settings(SECURE_CSP=value):
|
||||||
|
errors = base.check_csp_settings(None)
|
||||||
|
self.assertEqual(errors, [csp_error])
|
||||||
|
csp_report_only_error = Error(
|
||||||
|
base.E026.msg % ("SECURE_CSP_REPORT_ONLY", value), id=base.E026.id
|
||||||
|
)
|
||||||
|
with self.settings(SECURE_CSP_REPORT_ONLY=value):
|
||||||
|
errors = base.check_csp_settings(None)
|
||||||
|
self.assertEqual(errors, [csp_report_only_error])
|
||||||
|
with self.settings(SECURE_CSP=value, SECURE_CSP_REPORT_ONLY=value):
|
||||||
|
errors = base.check_csp_settings(None)
|
||||||
|
self.assertEqual(errors, [csp_error, csp_report_only_error])
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>CSP Nonce Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>CSP Nonce Test</h1>
|
||||||
|
<p>CSP Nonce is present: {{ csp_nonce }}</p>
|
||||||
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
console.log("This script is allowed to run due to the nonce.");
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
console.log("This script might be blocked by CSP if a nonce is required.");
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -2,7 +2,8 @@
|
|||||||
Tests for Django's bundled context processors.
|
Tests for Django's bundled context processors.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.test import SimpleTestCase, TestCase, override_settings
|
from django.test import SimpleTestCase, TestCase, modify_settings, override_settings
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@ -96,3 +97,65 @@ class DebugContextProcessorTests(TestCase):
|
|||||||
self.assertContains(response, "Third query list: 2")
|
self.assertContains(response, "Third query list: 2")
|
||||||
# Check queries for DB connection 'other'
|
# Check queries for DB connection 'other'
|
||||||
self.assertContains(response, "Fourth query list: 3")
|
self.assertContains(response, "Fourth query list: 3")
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
ROOT_URLCONF="context_processors.urls",
|
||||||
|
TEMPLATES=[
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.csp",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
MIDDLEWARE=[
|
||||||
|
"django.middleware.csp.ContentSecurityPolicyMiddleware",
|
||||||
|
],
|
||||||
|
SECURE_CSP={
|
||||||
|
"script-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
class CSPContextProcessorTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests for the django.template.context_processors.csp_nonce processor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_csp_nonce_in_context(self):
|
||||||
|
response = self.client.get("/csp_nonce/")
|
||||||
|
self.assertIn("csp_nonce", response.context)
|
||||||
|
|
||||||
|
@modify_settings(
|
||||||
|
MIDDLEWARE={"remove": "django.middleware.csp.ContentSecurityPolicyMiddleware"}
|
||||||
|
)
|
||||||
|
def test_csp_nonce_in_context_no_middleware(self):
|
||||||
|
response = self.client.get("/csp_nonce/")
|
||||||
|
self.assertIn("csp_nonce", response.context)
|
||||||
|
|
||||||
|
def test_csp_nonce_in_header(self):
|
||||||
|
response = self.client.get("/csp_nonce/")
|
||||||
|
self.assertIn(CSP.HEADER_ENFORCE, response.headers)
|
||||||
|
csp_header = response.headers[CSP.HEADER_ENFORCE]
|
||||||
|
nonce = response.context["csp_nonce"]
|
||||||
|
self.assertIn(f"'nonce-{nonce}'", csp_header)
|
||||||
|
|
||||||
|
def test_different_nonce_per_request(self):
|
||||||
|
response1 = self.client.get("/csp_nonce/")
|
||||||
|
response2 = self.client.get("/csp_nonce/")
|
||||||
|
self.assertNotEqual(
|
||||||
|
response1.context["csp_nonce"],
|
||||||
|
response2.context["csp_nonce"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_csp_nonce_in_template(self):
|
||||||
|
response = self.client.get("/csp_nonce/")
|
||||||
|
nonce = response.context["csp_nonce"]
|
||||||
|
self.assertIn(f'<script nonce="{nonce}">', response.text)
|
||||||
|
|
||||||
|
def test_csp_nonce_length(self):
|
||||||
|
response = self.client.get("/csp_nonce/")
|
||||||
|
nonce = response.context["csp_nonce"]
|
||||||
|
self.assertEqual(len(nonce), 22) # Based on secrets.token_urlsafe of 16 bytes.
|
||||||
|
@ -5,4 +5,5 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("request_attrs/", views.request_processor),
|
path("request_attrs/", views.request_processor),
|
||||||
path("debug/", views.debug_processor),
|
path("debug/", views.debug_processor),
|
||||||
|
path("csp_nonce/", views.csp_nonce_processor),
|
||||||
]
|
]
|
||||||
|
@ -13,3 +13,7 @@ def debug_processor(request):
|
|||||||
"other_debug_objects": DebugObject.objects.using("other"),
|
"other_debug_objects": DebugObject.objects.using("other"),
|
||||||
}
|
}
|
||||||
return render(request, "context_processors/debug.html", context)
|
return render(request, "context_processors/debug.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def csp_nonce_processor(request):
|
||||||
|
return render(request, "context_processors/csp_nonce.html")
|
||||||
|
135
tests/middleware/test_csp.py
Normal file
135
tests/middleware/test_csp.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from utils_tests.test_csp import basic_config, basic_policy
|
||||||
|
|
||||||
|
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.test.selenium import SeleniumTestCase
|
||||||
|
from django.test.utils import modify_settings, override_settings
|
||||||
|
from django.utils.csp import CSP
|
||||||
|
|
||||||
|
from .views import csp_reports
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
MIDDLEWARE=["django.middleware.csp.ContentSecurityPolicyMiddleware"],
|
||||||
|
ROOT_URLCONF="middleware.urls",
|
||||||
|
)
|
||||||
|
class CSPMiddlewareTest(SimpleTestCase):
|
||||||
|
@override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=None)
|
||||||
|
def test_csp_defaults_off(self):
|
||||||
|
response = self.client.get("/csp-base/")
|
||||||
|
self.assertNotIn(CSP.HEADER_ENFORCE, response)
|
||||||
|
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
|
||||||
|
|
||||||
|
@override_settings(SECURE_CSP=basic_config, SECURE_CSP_REPORT_ONLY=None)
|
||||||
|
def test_csp_basic(self):
|
||||||
|
"""
|
||||||
|
With SECURE_CSP set to a valid value, the middleware adds a
|
||||||
|
"Content-Security-Policy" header to the response.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-base/")
|
||||||
|
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
|
||||||
|
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
|
||||||
|
|
||||||
|
@override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]})
|
||||||
|
def test_csp_basic_with_nonce(self):
|
||||||
|
"""
|
||||||
|
Test the nonce is added to the header and matches what is in the view.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-nonce/")
|
||||||
|
nonce = response.text
|
||||||
|
self.assertTrue(nonce)
|
||||||
|
self.assertEqual(
|
||||||
|
response[CSP.HEADER_ENFORCE], f"default-src 'self' 'nonce-{nonce}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(SECURE_CSP={"default-src": [CSP.SELF, CSP.NONCE]})
|
||||||
|
def test_csp_basic_with_nonce_but_unused(self):
|
||||||
|
"""
|
||||||
|
Test if `request.csp_nonce` is never accessed, it is not added to the header.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-base/")
|
||||||
|
nonce = response.text
|
||||||
|
self.assertIsNotNone(nonce)
|
||||||
|
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
|
||||||
|
|
||||||
|
@override_settings(SECURE_CSP=None, SECURE_CSP_REPORT_ONLY=basic_config)
|
||||||
|
def test_csp_report_only_basic(self):
|
||||||
|
"""
|
||||||
|
With SECURE_CSP_REPORT_ONLY set to a valid value, the middleware adds a
|
||||||
|
"Content-Security-Policy-Report-Only" header to the response.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-base/")
|
||||||
|
self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
|
||||||
|
self.assertNotIn(CSP.HEADER_ENFORCE, response)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
SECURE_CSP=basic_config,
|
||||||
|
SECURE_CSP_REPORT_ONLY=basic_config,
|
||||||
|
)
|
||||||
|
def test_csp_both(self):
|
||||||
|
"""
|
||||||
|
If both SECURE_CSP and SECURE_CSP_REPORT_ONLY are set, the middleware
|
||||||
|
adds both headers to the response.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-base/")
|
||||||
|
self.assertEqual(response[CSP.HEADER_ENFORCE], basic_policy)
|
||||||
|
self.assertEqual(response[CSP.HEADER_REPORT_ONLY], basic_policy)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
DEBUG=True,
|
||||||
|
SECURE_CSP=basic_config,
|
||||||
|
SECURE_CSP_REPORT_ONLY=basic_config,
|
||||||
|
)
|
||||||
|
def test_csp_404_debug_view(self):
|
||||||
|
"""
|
||||||
|
Test that the CSP headers are not added to the debug view.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-404/")
|
||||||
|
self.assertNotIn(CSP.HEADER_ENFORCE, response)
|
||||||
|
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
DEBUG=True,
|
||||||
|
SECURE_CSP=basic_config,
|
||||||
|
SECURE_CSP_REPORT_ONLY=basic_config,
|
||||||
|
)
|
||||||
|
def test_csp_500_debug_view(self):
|
||||||
|
"""
|
||||||
|
Test that the CSP headers are not added to the debug view.
|
||||||
|
"""
|
||||||
|
response = self.client.get("/csp-500/")
|
||||||
|
self.assertNotIn(CSP.HEADER_ENFORCE, response)
|
||||||
|
self.assertNotIn(CSP.HEADER_REPORT_ONLY, response)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
ROOT_URLCONF="middleware.urls",
|
||||||
|
SECURE_CSP_REPORT_ONLY={
|
||||||
|
"default-src": [CSP.NONE],
|
||||||
|
"img-src": [CSP.SELF],
|
||||||
|
"script-src": [CSP.SELF],
|
||||||
|
"style-src": [CSP.SELF],
|
||||||
|
"report-uri": "/csp-report/",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@modify_settings(
|
||||||
|
MIDDLEWARE={"append": "django.middleware.csp.ContentSecurityPolicyMiddleware"}
|
||||||
|
)
|
||||||
|
class CSPSeleniumTestCase(SeleniumTestCase, StaticLiveServerTestCase):
|
||||||
|
available_apps = ["middleware"]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.addCleanup(csp_reports.clear)
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def test_reports_are_generated(self):
|
||||||
|
url = self.live_server_url + "/csp-failure/"
|
||||||
|
self.selenium.get(url)
|
||||||
|
time.sleep(1) # Allow time for the CSP report to be sent.
|
||||||
|
reports = sorted(
|
||||||
|
(r["csp-report"]["document-uri"], r["csp-report"]["violated-directive"])
|
||||||
|
for r in csp_reports
|
||||||
|
)
|
||||||
|
self.assertEqual(reports, [(url, "img-src"), (url, "style-src-elem")])
|
@ -1,4 +1,5 @@
|
|||||||
from django.urls import path, re_path
|
from django.urls import path, re_path
|
||||||
|
from django.views.debug import default_urlconf
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
@ -11,4 +12,10 @@ urlpatterns = [
|
|||||||
# Should not append slash.
|
# Should not append slash.
|
||||||
path("sensitive_fbv/", views.sensitive_fbv),
|
path("sensitive_fbv/", views.sensitive_fbv),
|
||||||
path("sensitive_cbv/", views.SensitiveCBV.as_view()),
|
path("sensitive_cbv/", views.SensitiveCBV.as_view()),
|
||||||
|
# Used in CSP tests.
|
||||||
|
path("csp-failure/", default_urlconf),
|
||||||
|
path("csp-report/", views.csp_report_view),
|
||||||
|
path("csp-base/", views.empty_view),
|
||||||
|
path("csp-nonce/", views.csp_nonce),
|
||||||
|
path("csp-500/", views.csp_500),
|
||||||
]
|
]
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
from django.middleware.csp import get_nonce
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.debug import technical_500_response
|
||||||
from django.views.decorators.common import no_append_slash
|
from django.views.decorators.common import no_append_slash
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
|
|
||||||
@ -17,3 +23,25 @@ def sensitive_fbv(request, *args, **kwargs):
|
|||||||
class SensitiveCBV(View):
|
class SensitiveCBV(View):
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
def csp_nonce(request):
|
||||||
|
return HttpResponse(get_nonce(request))
|
||||||
|
|
||||||
|
|
||||||
|
def csp_500(request):
|
||||||
|
try:
|
||||||
|
raise Exception
|
||||||
|
except Exception:
|
||||||
|
return technical_500_response(request, *sys.exc_info())
|
||||||
|
|
||||||
|
|
||||||
|
csp_reports = []
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def csp_report_view(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
data = json.loads(request.body)
|
||||||
|
csp_reports.append(data)
|
||||||
|
return HttpResponse(status=204)
|
||||||
|
166
tests/utils_tests/test_csp.py
Normal file
166
tests/utils_tests/test_csp.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
from secrets import token_urlsafe
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.utils.csp import CSP, LazyNonce, build_policy
|
||||||
|
from django.utils.functional import empty
|
||||||
|
|
||||||
|
basic_config = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
}
|
||||||
|
alt_config = {
|
||||||
|
"default-src": [CSP.SELF, CSP.UNSAFE_INLINE],
|
||||||
|
}
|
||||||
|
basic_policy = "default-src 'self'"
|
||||||
|
|
||||||
|
|
||||||
|
class CSPConstantsTests(SimpleTestCase):
|
||||||
|
def test_constants(self):
|
||||||
|
self.assertEqual(CSP.NONE, "'none'")
|
||||||
|
self.assertEqual(CSP.REPORT_SAMPLE, "'report-sample'")
|
||||||
|
self.assertEqual(CSP.SELF, "'self'")
|
||||||
|
self.assertEqual(CSP.STRICT_DYNAMIC, "'strict-dynamic'")
|
||||||
|
self.assertEqual(CSP.UNSAFE_EVAL, "'unsafe-eval'")
|
||||||
|
self.assertEqual(CSP.UNSAFE_HASHES, "'unsafe-hashes'")
|
||||||
|
self.assertEqual(CSP.UNSAFE_INLINE, "'unsafe-inline'")
|
||||||
|
self.assertEqual(CSP.WASM_UNSAFE_EVAL, "'wasm-unsafe-eval'")
|
||||||
|
self.assertEqual(CSP.NONCE, "<CSP_NONCE_SENTINEL>")
|
||||||
|
|
||||||
|
|
||||||
|
class CSPBuildPolicyTest(SimpleTestCase):
|
||||||
|
|
||||||
|
def assertPolicyEqual(self, a, b):
|
||||||
|
parts_a = sorted(a.split("; ")) if a is not None else None
|
||||||
|
parts_b = sorted(b.split("; ")) if b is not None else None
|
||||||
|
self.assertEqual(parts_a, parts_b, f"Policies not equal: {a!r} != {b!r}")
|
||||||
|
|
||||||
|
def test_config_empty(self):
|
||||||
|
self.assertPolicyEqual(build_policy({}), "")
|
||||||
|
|
||||||
|
def test_config_basic(self):
|
||||||
|
self.assertPolicyEqual(build_policy(basic_config), basic_policy)
|
||||||
|
|
||||||
|
def test_config_multiple_directives(self):
|
||||||
|
policy = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
"script-src": [CSP.NONE],
|
||||||
|
}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy), "default-src 'self'; script-src 'none'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_value_as_string(self):
|
||||||
|
"""
|
||||||
|
Test that a single value can be passed as a string.
|
||||||
|
"""
|
||||||
|
policy = {"default-src": CSP.SELF}
|
||||||
|
self.assertPolicyEqual(build_policy(policy), "default-src 'self'")
|
||||||
|
|
||||||
|
def test_config_value_as_tuple(self):
|
||||||
|
"""
|
||||||
|
Test that a tuple can be passed as a value.
|
||||||
|
"""
|
||||||
|
policy = {"default-src": (CSP.SELF, "foo.com")}
|
||||||
|
self.assertPolicyEqual(build_policy(policy), "default-src 'self' foo.com")
|
||||||
|
|
||||||
|
def test_config_value_as_set(self):
|
||||||
|
"""
|
||||||
|
Test that a set can be passed as a value.
|
||||||
|
|
||||||
|
Sets are often used in Django settings to ensure uniqueness, however, sets are
|
||||||
|
unordered. The middleware ensures consistency via sorting if a set is passed.
|
||||||
|
"""
|
||||||
|
policy = {"default-src": {CSP.SELF, "foo.com", "bar.com"}}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy), "default-src 'self' bar.com foo.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_value_none(self):
|
||||||
|
"""
|
||||||
|
Test that `None` removes the directive from the policy.
|
||||||
|
|
||||||
|
Useful in cases where the CSP config is scripted in some way or
|
||||||
|
explicitly not wanting to set a directive.
|
||||||
|
"""
|
||||||
|
policy = {"default-src": [CSP.SELF], "script-src": None}
|
||||||
|
self.assertPolicyEqual(build_policy(policy), basic_policy)
|
||||||
|
|
||||||
|
def test_config_value_boolean_true(self):
|
||||||
|
policy = {"default-src": [CSP.SELF], "block-all-mixed-content": True}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy), "default-src 'self'; block-all-mixed-content"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_value_boolean_false(self):
|
||||||
|
policy = {"default-src": [CSP.SELF], "block-all-mixed-content": False}
|
||||||
|
self.assertPolicyEqual(build_policy(policy), basic_policy)
|
||||||
|
|
||||||
|
def test_config_value_multiple_boolean(self):
|
||||||
|
policy = {
|
||||||
|
"default-src": [CSP.SELF],
|
||||||
|
"block-all-mixed-content": True,
|
||||||
|
"upgrade-insecure-requests": True,
|
||||||
|
}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy),
|
||||||
|
"default-src 'self'; block-all-mixed-content; upgrade-insecure-requests",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_with_nonce_arg(self):
|
||||||
|
"""
|
||||||
|
Test when the `CSP.NONCE` is not in the defined policy, the nonce
|
||||||
|
argument has no effect.
|
||||||
|
"""
|
||||||
|
self.assertPolicyEqual(build_policy(basic_config, nonce="abc123"), basic_policy)
|
||||||
|
|
||||||
|
def test_config_with_nonce(self):
|
||||||
|
policy = {"default-src": [CSP.SELF, CSP.NONCE]}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy, nonce="abc123"),
|
||||||
|
"default-src 'self' 'nonce-abc123'",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_with_multiple_nonces(self):
|
||||||
|
policy = {
|
||||||
|
"default-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
"script-src": [CSP.SELF, CSP.NONCE],
|
||||||
|
}
|
||||||
|
self.assertPolicyEqual(
|
||||||
|
build_policy(policy, nonce="abc123"),
|
||||||
|
"default-src 'self' 'nonce-abc123'; script-src 'self' 'nonce-abc123'",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_with_empty_directive(self):
|
||||||
|
policy = {"default-src": []}
|
||||||
|
self.assertPolicyEqual(build_policy(policy), "")
|
||||||
|
|
||||||
|
|
||||||
|
class LazyNonceTests(SimpleTestCase):
|
||||||
|
def test_generates_on_usage(self):
|
||||||
|
generated_tokens = []
|
||||||
|
nonce = LazyNonce()
|
||||||
|
self.assertFalse(nonce)
|
||||||
|
self.assertIs(nonce._wrapped, empty)
|
||||||
|
|
||||||
|
def memento_token_urlsafe(size):
|
||||||
|
generated_tokens.append(result := token_urlsafe(size))
|
||||||
|
return result
|
||||||
|
|
||||||
|
with patch("django.utils.csp.secrets.token_urlsafe", memento_token_urlsafe):
|
||||||
|
# Force usage, similar to template rendering, to generate the nonce.
|
||||||
|
val = str(nonce)
|
||||||
|
|
||||||
|
self.assertTrue(nonce)
|
||||||
|
self.assertEqual(nonce, val)
|
||||||
|
self.assertIsInstance(nonce, str)
|
||||||
|
self.assertEqual(len(val), 22) # Based on secrets.token_urlsafe of 16 bytes.
|
||||||
|
self.assertEqual(generated_tokens, [nonce])
|
||||||
|
# Also test the wrapped value.
|
||||||
|
self.assertEqual(nonce._wrapped, val)
|
||||||
|
|
||||||
|
def test_returns_same_value(self):
|
||||||
|
nonce = LazyNonce()
|
||||||
|
first = str(nonce)
|
||||||
|
second = str(nonce)
|
||||||
|
|
||||||
|
self.assertEqual(first, second)
|
Loading…
x
Reference in New Issue
Block a user