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 empty Content-Security-Policy header (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 Jacob Walls, 3 weeks ago

Cc: Rob Hudson added

I'm not certain that recommending a fallback is a good idea in all cases. Rob, do you have thoughts?

comment:2 by NAYAN SHARMA, 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.

comment:3 by Jacob Walls, 12 days ago

It's not accepted yet, which means we don't even know if we want this yet.

Last edited 12 days ago by Jacob Walls (previous) (diff)

in reply to:  3 comment:4 by NAYAN SHARMA, 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 Sanskar Kharya, 6 days ago

Has patch: set
Owner: set to Sanskar Kharya
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 Sarah Boyce, 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 Rob Hudson, 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 Jacob Walls, 5 days ago

Component: Core (System checks) → Core (Other)
Type: Cleanup/optimization → Bug

comment:9 by Jacob Walls, 5 days ago

Component: Core (Other) → CSP

comment:10 by Sanskar Kharya, 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 Jacob Walls, 3 days ago

Patch needs improvement: unset
Triage Stage: Accepted → Ready for checkin
Version: dev → 6.0

comment:12 by Jacob Walls <jacobtylerwalls@…>, 3 days ago

Resolution: → fixed
Status: assigned → closed

In 549605de:

Fixed #37395 -- Emitted 'none' for CSP directives left empty by an unused nonce.

Before, if a directive's only source was a nonce, and that nonce was not rendered,
the directive was omitted, when in reality we should consider the set of scripts
matching the nonce to be empty, and instead fail closed.

Thanks Rob Hudson for the implementation idea and review.

Note: See TracTickets for help on using tickets.
Back to Top