Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reading nonce if it was included in header #269

Merged
merged 3 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions csp/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ class PolicyParts:
nonce: str | None = None


class FalseLazyObject(SimpleLazyObject):
def __bool__(self) -> bool:
return False


class CSPMiddleware(MiddlewareMixin):
"""
Implements the Content-Security-Policy response header, which
Expand All @@ -40,6 +45,8 @@ class CSPMiddleware(MiddlewareMixin):
Can be customised by subclassing and extending the get_policy_parts method.
"""

always_generate_nonce = False

def _make_nonce(self, request: HttpRequest) -> str:
# Ensure that any subsequent calls to request.csp_nonce return the same value
stored_nonce = getattr(request, "_csp_nonce", None)
Expand All @@ -58,6 +65,8 @@ def _csp_nonce_post_response() -> None:
def process_request(self, request: HttpRequest) -> None:
nonce = partial(self._make_nonce, request)
setattr(request, "csp_nonce", SimpleLazyObject(nonce))
if self.always_generate_nonce:
self._make_nonce(request)

def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase:
# Check for debug view
Expand Down Expand Up @@ -95,7 +104,8 @@ def process_response(self, request: HttpRequest, response: HttpResponseBase) ->
# Once we've written the header, accessing the `request.csp_nonce` will no longer trigger
# the nonce to be added to the header. Instead we throw an error here to catch this since
# this has security implications.
setattr(request, "csp_nonce", SimpleLazyObject(self._csp_nonce_post_response))
if getattr(request, "_csp_nonce", None) is None:
setattr(request, "csp_nonce", FalseLazyObject(self._csp_nonce_post_response))

return response

Expand All @@ -109,7 +119,12 @@ def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> s
policy_parts_ro = self.get_policy_parts(request=request, response=response, report_only=True)
return build_policy(**asdict(policy_parts_ro), report_only=True)

def get_policy_parts(self, request: HttpRequest, response: HttpResponseBase, report_only: bool = False) -> PolicyParts:
def get_policy_parts(
self,
request: HttpRequest,
response: HttpResponseBase,
report_only: bool = False,
) -> PolicyParts:
if report_only:
config = getattr(response, "_csp_config_ro", None)
update = getattr(response, "_csp_update_ro", None)
Expand All @@ -122,3 +137,17 @@ def get_policy_parts(self, request: HttpRequest, response: HttpResponseBase, rep
nonce = getattr(request, "_csp_nonce", None)

return PolicyParts(config, update, replace, nonce)


class CSPMiddlewareAlwaysGenerateNonce(CSPMiddleware):
"""
A middleware variant that always generates a nonce.

This is useful when a later process needs a nonce, whether or not the wrapped
request uses a nonce. One example is django-debug-toolbar (DDT). The DDT
middleware needs to be high in the MIDDLEWARE list, so it can inject its
HTML, CSS, and JS describing the response generation. DDT users can use
this middleware instead of CSPMiddleware.
"""

always_generate_nonce = True
56 changes: 53 additions & 3 deletions csp/tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from csp.constants import HEADER, HEADER_REPORT_ONLY, SELF
from csp.exceptions import CSPNonceError
from csp.middleware import CSPMiddleware
from csp.middleware import CSPMiddleware, CSPMiddlewareAlwaysGenerateNonce
from csp.tests.utils import response

mw = CSPMiddleware(response())
Expand Down Expand Up @@ -160,10 +160,60 @@ def test_nonce_regenerated_on_new_request() -> None:
assert nonce2 not in response1[HEADER]


def test_nonce_attribute_error() -> None:
# Test `CSPNonceError` is raised when accessing the nonce after the response has been processed.
def test_no_nonce_access_after_middleware_is_attribute_error() -> None:
# Test `CSPNonceError` is raised when accessing an unset nonce after the response has been processed.
request = rf.get("/")
mw.process_request(request)
mw.process_response(request, HttpResponse())
assert bool(getattr(request, "csp_nonce", True)) is False
with pytest.raises(CSPNonceError):
str(getattr(request, "csp_nonce"))


def test_set_nonce_access_after_middleware_is_ok() -> None:
# Test accessing a set nonce after the response has been processed is OK.
request = rf.get("/")
mw.process_request(request)
nonce = str(getattr(request, "csp_nonce"))
mw.process_response(request, HttpResponse())
assert bool(getattr(request, "csp_nonce", False)) is True
assert str(getattr(request, "csp_nonce")) == nonce


def test_csp_always_nonce_middleware_has_nonce() -> None:
request = rf.get("/")
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
mw_agn.process_request(request)
resp = HttpResponse()
mw_agn.process_response(request, resp)
nonce = str(getattr(request, "csp_nonce"))
assert nonce in resp[HEADER]


def test_csp_always_nonce_middleware_nonce_regenerated_on_new_request() -> None:
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
request1 = rf.get("/")
request2 = rf.get("/")
mw_agn.process_request(request1)
mw_agn.process_request(request2)
nonce1 = str(getattr(request1, "csp_nonce"))
nonce2 = str(getattr(request2, "csp_nonce"))
assert nonce1 != nonce2

response1 = HttpResponse()
response2 = HttpResponse()
mw_agn.process_response(request1, response1)
mw_agn.process_response(request2, response2)
assert nonce1 not in response2[HEADER]
assert nonce2 not in response1[HEADER]


def test_csp_always_nonce_middleware_access_after_middleware_is_ok() -> None:
# Test accessing a set nonce after the response has been processed is OK.
request = rf.get("/")
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
mw_agn.process_request(request)
nonce = str(getattr(request, "csp_nonce"))
mw_agn.process_response(request, HttpResponse())
assert bool(getattr(request, "csp_nonce", False)) is True
assert str(getattr(request, "csp_nonce")) == nonce
7 changes: 6 additions & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ to ``MIDDLEWARE``, like so:
# ...
)

Note: Middleware order does not matter unless you have other middleware modifying the CSP header.
.. Note::

Middleware order does not matter unless you have other middleware modifying
the CSP header, or requires CSP features like a nonce. See
:ref:`Using the generated CSP nonce` for further advice on middleware order.


That should do it! Go on to :ref:`configuring CSP <configuration-chapter>`.
43 changes: 37 additions & 6 deletions docs/nonce.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,44 @@ above script being allowed.
- ``request.csp_nonce`` is accessed during the request lifecycle, after the middleware
processes the request but before it processes the response.

If ``request.csp_nonce`` is accessed **after** the response has been processed by the middleware,
a ``csp.exceptions.CSPNonceError`` will be raised.
If the nonce was not generated and included in the CSP header, then accessing ``request.csp_nonce``
could indicate a programming error. To help identify the error, testing
(like ``bool(request.csp_nonce)``) will evaluate to ``False``, and reading
(like ``str(request.csp_nonce)``) will raise a
``csp.exceptions.CSPNonceError``.

Middleware that accesses ``request.csp_nonce`` **must be placed after**
``csp.middleware.CSPMiddleware`` in the ``MIDDLEWARE`` setting. This ensures that
``CSPMiddleware`` properly processes the response and includes the nonce in the CSP header before
other middleware attempts to use it.
If the nonce was generated and included in the CSP header, then accessing ``request.csp_nonce``
is safe. Testing (like ``bool(request.csp_nonce)``) will evaluate to
``True``, and reading (like ``str(request.csp_nonce)``) will return the nonce.

If other middleware or a later process needs to access ``request.csp_nonce``, then there are a few options:

* The middleware can be placed after ``csp.middleware.CSPMiddleware`` in the ``MIDDLEWARE`` setting.
This ensures that the middleware generates the nonce before ``CSPMiddleware`` writes the CSP header.
* Use the alternate ``csp.middleware.CSPMiddlewareAlwaysGenerateNonce`` middleware, which always
generates a nonce and includes it in the CSP header.
* Add a later middleware that accesses the nonce. For example, this function:

.. code-block:: python

def init_csp_nonce_middleware(get_response):
def middleware(request):
getattr(request, "csp_nonce", None)
return get_response(request)

return middleware

could be added to the ``MIDDLEWARE`` list:

.. code-block:: python

MIDDLEWARE = (
"my.middleware.ThatUsesCSPNonce",
# ...
"csp.middleware.CSPMiddleware",
# ...
"my.middleware.init_csp_nonce_middleware",
)

``Context Processor``
=====================
Expand Down