Opened 3 weeks ago
Closed 3 days ago
#37395 closed Bug (fixed)
Nonce-only CSP directives are omitted when the nonce is unused
| Reported by: | venkatchalla06 | Owned by: | Sanskar Kharya |
|---|---|---|---|
| Component: | CSP | Version: | 6.0 |
| Severity: | Normal | Keywords: | |
| Cc: | venkatchalla06, Rob Hudson | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Django 6.0 added built-in CSP. In django.utils.csp.build_policy(), when a directive's only source is CSP.NONCE and the per-request nonce was never accessed (the LazyNonce stays falsy because the response rendered no {{ csp_nonce }}), the sentinel is stripped, the directive becomes empty, and the directive is dropped from the header entirely. For the same configuration, the emitted policy then depends on whether the nonce happened to be used:
{"default-src": ["*"], "script-src": [CSP.NONCE]}— nonce used:default-src *; script-src 'nonce-...'; nonce unused:default-src *(script-src removed, scripts fall back to*).{"script-src": [CSP.NONCE]}— nonce unused: an emptyContent-Security-Policyheader (no policy at all).{"script-src": [CSP.SELF, CSP.NONCE]}(recommended) — nonce unused:script-src 'self'(fails closed, unaffected).
So a nonce-only directive silently degrades to its default-src fallback (possibly permissive) or to no policy, on exactly the responses most likely to carry injected markup (error pages, views with no inline nonce'd element). The recommended form — pairing CSP.NONCE with an explicit fallback — already fails closed.
Proposed: a new system check, security.W028, that warns when any SECURE_CSP or SECURE_CSP_REPORT_ONLY directive's only source is CSP.NONCE, hinting to add CSP.SELF, CSP.STRICT_DYNAMIC, or CSP.NONE. It mirrors the existing security.W027 and makes no runtime behavior change. A patch with tests and docs is ready.
An alternative would be to make build_policy() fail closed (keep the directive present as block-all when the nonce is unused), but that changes documented output and would need a release note; a check seems the lower-risk fix.
Change History (12)
comment:1 by , 3 weeks ago
| Cc: | added |
|---|
comment:2 by , 12 days ago
Hi, I'd like to contribute to this ticket. I noticed the description mentions that a patch with tests and documentation is ready, although the ticket currently shows “Has patch: no.”
Is someone already preparing or reviewing that patch? If help is still needed, I'd be happy to test the proposed behaviour and look for missing edge cases before working on a separate implementation.
follow-up: 4 comment:3 by , 12 days ago
It's not accepted yet, which means we don't even know if we want this yet.
comment:4 by , 10 days ago
Hi Jacob, would you mind taking a look at my PR for ticket #37414 when you have some time? I’d appreciate any feedback or suggestions for improvement. Thank you! Replying to Jacob Walls:
It's not accepted yet, which means we don't even know if we want this yet.
comment:5 by , 6 days ago
| Has patch: | set |
|---|---|
| Owner: | set to |
| Status: | new → assigned |
Pull request: https://github.com/django/django/pull/22179
This adds a new system check, security.W028, that warns when a CSP directive's only source is CSP.NONCE, since that directive is dropped from the header when the nonce is never used. Tests and docs included.
comment:6 by , 6 days ago
| Patch needs improvement: | set |
|---|---|
| Summary: | Add a system check for a CSP directive whose only source is CSP.NONCE (silently fails open) → Nonce-only CSP directives are omitted when the nonce is unused |
| Triage Stage: | Unreviewed → Accepted |
| Type: | New feature → Cleanup/optimization |
#37441 was a duplicate
Thank you for the report! I agree we should do something here
The best approach needs further consideration, so I’ve updated the title to describe the issue without prescribing a system check.
See also #37419, where we decided to generate configured CSP nonces eagerly for streaming responses so they are included in the headers.
We should consider whether to do the same when CSP.NONCE is a directive’s only source.
comment:7 by , 5 days ago
This is an interesting edge case. I would recommend emitting csp.NONE when removing the nonce sentinel leaves a directive empty. If no element carries the nonce, the set of scripts matching the nonce is empty, which is exactly what 'none' means and it's a much better fallback than empty where the policy fails open.
comment:8 by , 5 days ago
| Component: | Core (System checks) → Core (Other) |
|---|---|
| Type: | Cleanup/optimization → Bug |
comment:9 by , 5 days ago
| Component: | Core (Other) → CSP |
|---|
comment:10 by , 5 days ago
Thanks Rob and Jacob. I've updated the PR to emit 'none' when removing the nonce sentinel leaves a directive empty, instead of eagerly generating the nonce. Directives that are explicitly configured empty are still omitted, and directives with other sources are unchanged. Tests and docs updated.
comment:11 by , 3 days ago
| Patch needs improvement: | unset |
|---|---|
| Triage Stage: | Accepted → Ready for checkin |
| Version: | dev → 6.0 |
I'm not certain that recommending a fallback is a good idea in all cases. Rob, do you have thoughts?