Skip to content

Commit 01a2b0f

Browse files
committed
Add CSPMiddlewareAlwaysGenerateNonce
This variant middleware always generates the nonce. This is useful when a process that runs after the middleware needs the nonce. One example is the middleware used by django-debug-toolbar (DDT). It needs to be defined early in the MIDDLEWARE list that it can inject HTML, CSS, and JavaScript after the response has been generated. DDT users could use this middleware to ensure the CSP nonce is always available for its asset.
1 parent 1b7161a commit 01a2b0f

File tree

4 files changed

+101
-8
lines changed

4 files changed

+101
-8
lines changed

csp/middleware.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class CSPMiddleware(MiddlewareMixin):
4545
Can be customised by subclassing and extending the get_policy_parts method.
4646
"""
4747

48+
always_generate_nonce = False
49+
4850
def _make_nonce(self, request: HttpRequest) -> str:
4951
# Ensure that any subsequent calls to request.csp_nonce return the same value
5052
stored_nonce = getattr(request, "_csp_nonce", None)
@@ -63,6 +65,8 @@ def _csp_nonce_post_response() -> None:
6365
def process_request(self, request: HttpRequest) -> None:
6466
nonce = partial(self._make_nonce, request)
6567
setattr(request, "csp_nonce", SimpleLazyObject(nonce))
68+
if self.always_generate_nonce:
69+
self._make_nonce(request)
6670

6771
def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase:
6872
# Check for debug view
@@ -133,3 +137,17 @@ def get_policy_parts(
133137
nonce = getattr(request, "_csp_nonce", None)
134138

135139
return PolicyParts(config, update, replace, nonce)
140+
141+
142+
class CSPMiddlewareAlwaysGenerateNonce(CSPMiddleware):
143+
"""
144+
A middleware variant that always generates a nonce.
145+
146+
This is useful when a later process needs a nonce, whether or not the wrapped
147+
request uses a nonce. One example is django-debug-toolbar (DDT). The DDT
148+
middleware needs to be high in the MIDDLEWARE list, so it can inject its
149+
HTML, CSS, and JS describing the request generation. DDT users can use
150+
this middleware instead of CSPMiddleware.
151+
"""
152+
153+
always_generate_nonce = True

csp/tests/test_middleware.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from csp.constants import HEADER, HEADER_REPORT_ONLY, SELF
1212
from csp.exceptions import CSPNonceError
13-
from csp.middleware import CSPMiddleware
13+
from csp.middleware import CSPMiddleware, CSPMiddlewareAlwaysGenerateNonce
1414
from csp.tests.utils import response
1515

1616
mw = CSPMiddleware(response())
@@ -178,3 +178,42 @@ def test_set_nonce_access_after_middleware_is_ok() -> None:
178178
mw.process_response(request, HttpResponse())
179179
assert bool(getattr(request, "csp_nonce", False)) is True
180180
assert str(getattr(request, "csp_nonce")) == nonce
181+
182+
183+
def test_csp_always_nonce_middleware_has_nonce() -> None:
184+
request = rf.get("/")
185+
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
186+
mw_agn.process_request(request)
187+
resp = HttpResponse()
188+
mw_agn.process_response(request, resp)
189+
nonce = str(getattr(request, "csp_nonce"))
190+
assert nonce in resp[HEADER]
191+
192+
193+
def test_csp_always_nonce_middleware_nonce_regenerated_on_new_request() -> None:
194+
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
195+
request1 = rf.get("/")
196+
request2 = rf.get("/")
197+
mw_agn.process_request(request1)
198+
mw_agn.process_request(request2)
199+
nonce1 = str(getattr(request1, "csp_nonce"))
200+
nonce2 = str(getattr(request2, "csp_nonce"))
201+
assert nonce1 != nonce2
202+
203+
response1 = HttpResponse()
204+
response2 = HttpResponse()
205+
mw_agn.process_response(request1, response1)
206+
mw_agn.process_response(request2, response2)
207+
assert nonce1 not in response2[HEADER]
208+
assert nonce2 not in response1[HEADER]
209+
210+
211+
def test_csp_always_nonce_middleware_access_after_middleware_is_ok() -> None:
212+
# Test accessing a set nonce after the response has been processed is OK.
213+
request = rf.get("/")
214+
mw_agn = CSPMiddlewareAlwaysGenerateNonce(response())
215+
mw_agn.process_request(request)
216+
nonce = str(getattr(request, "csp_nonce"))
217+
mw_agn.process_response(request, HttpResponse())
218+
assert bool(getattr(request, "csp_nonce", False)) is True
219+
assert str(getattr(request, "csp_nonce")) == nonce

docs/installation.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ to ``MIDDLEWARE``, like so:
3131
# ...
3232
)
3333
34-
Note: Middleware order does not matter unless you have other middleware modifying the CSP header.
34+
.. Note::
35+
36+
Middleware order does not matter unless you have other middleware modifying
37+
the CSP header, or requires CSP features like a nonce. See
38+
:ref:`Using the generated CSP nonce` for further advice on middleware order.
39+
3540

3641
That should do it! Go on to :ref:`configuring CSP <configuration-chapter>`.

docs/nonce.rst

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,44 @@ above script being allowed.
4848
- ``request.csp_nonce`` is accessed during the request lifecycle, after the middleware
4949
processes the request but before it processes the response.
5050

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

54-
Middleware that accesses ``request.csp_nonce`` **must be placed after**
55-
``csp.middleware.CSPMiddleware`` in the ``MIDDLEWARE`` setting. This ensures that
56-
``CSPMiddleware`` properly processes the response and includes the nonce in the CSP header before
57-
other middleware attempts to use it.
57+
If the nonce was generated and included in the CSP header, then accessing ``request.csp_nonce``
58+
is safe. Testing (like ``bool(request.csp_nonce)``) will evaluate to
59+
``True``, and reading (like ``str(request.csp_nonce)``) will return the nonce.
60+
61+
If other middleware or a later process needs to access ``request.csp_nonce``, then there are a few options:
62+
63+
* The middleware can be placed after ``csp.middleware.CSPMiddleware`` in the ``MIDDLEWARE`` setting.
64+
This ensures that the middleware generates the nonce before ``CSPMiddleware`` writes the CSP header.
65+
* Use the alternate ``csp.middleware.CSPMiddlewareAlwaysGenerateNonce`` middleware, which always
66+
generates a nonce and includes it in the CSP header.
67+
* Add a later middleware that accesses the nonce. For example, this function:
68+
69+
.. code-block:: python
70+
71+
def init_csp_nonce_middleware(get_response):
72+
def middleware(request):
73+
getattr(request, "csp_nonce", None)
74+
return get_response(request)
75+
76+
return middleware
77+
78+
could be added to the ``MIDDLEWARE`` list:
79+
80+
.. code-block:: python
81+
82+
MIDDLEWARE = (
83+
"my.middleware.ThatUsesCSPNonce",
84+
# ...
85+
"csp.middleware.CSPMiddleware",
86+
# ...
87+
"my.middleware.init_csp_nonce_middleware",
88+
)
5889
5990
``Context Processor``
6091
=====================

0 commit comments

Comments
 (0)