Metadata-Version: 2.4
Name: django-pipeline-csp
Version: 0.3.0
Summary: CSP nonce support for django-pipeline script and stylesheet tags
Author-email: Christian Bergmann <christian.bergmann@cnbg-n.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/Feuerwurmcx/django-pipeline-csp
Project-URL: Issues, https://github.com/Feuerwurmcx/django-pipeline-csp/issues
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Classifier: Framework :: Django :: 5.1
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: django>=4.2
Requires-Dist: django-pipeline<5,>=4.1
Provides-Extra: django-csp
Requires-Dist: django-csp>=4.0; extra == "django-csp"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-django>=4.9; extra == "test"
Requires-Dist: coverage[toml]>=7; extra == "test"
Requires-Dist: django-csp>=4.0; extra == "test"
Provides-Extra: dev
Requires-Dist: django-pipeline-csp[test]; extra == "dev"
Requires-Dist: nox; extra == "dev"
Requires-Dist: ruff~=0.15.0; extra == "dev"
Requires-Dist: bump-my-version; extra == "dev"
Dynamic: license-file

# django-pipeline-csp

CSP nonce support for [django-pipeline](https://github.com/jazzband/django-pipeline) script and stylesheet tags.

Under a nonce-based Content Security Policy with `'strict-dynamic'`, browsers ignore host allowlists, so every
`<script>` needs the request's nonce. django-pipeline renders its tags without one
([jazzband/django-pipeline#771](https://github.com/jazzband/django-pipeline/issues/771)). `django-pipeline-csp` provides a
drop-in `{% javascript %}` tag that adds it.

## Installation

```bash
pip install django-pipeline-csp
```

```python
INSTALLED_APPS = [
    # ...
    "pipeline",
    "pipeline_csp",
]
```

## Usage

Replace `{% load pipeline %}` with `{% load pipeline_csp %}`:

```django
{% load pipeline_csp %}
{% stylesheet "base" %}
{% javascript "polyfills" %}
```

Every `<script>` rendered by `{% javascript %}` gets `nonce="..."` — individual source files
(`PIPELINE_ENABLED = False`), the compressed bundle and inline JavaScript templates alike. Likewise every `<link>`
rendered by `{% stylesheet %}`.

Do not load both libraries in one template: the library loaded last wins, and with
`{% load pipeline_csp pipeline %}` the nonce is silently missing.

## Form and widget media (`class Media`)

Django renders `{{ form.media }}` itself, so those `<link>` and `<script>` tags have no nonce either — including
`PipelineFormMedia` with `css_packages` / `js_packages`. There are two ways to add it.

### Everywhere: `MediaNonceMiddleware`

Add the middleware **after** your CSP middleware:

```python
MIDDLEWARE = [
    # ...
    "csp.middleware.CSPMiddleware",  # or django.middleware.csp.ContentSecurityPolicyMiddleware
    "pipeline_csp.middleware.MediaNonceMiddleware",
]
```

Every `<link>` and `<script>` rendered by a `Media` object during the request gets the nonce: your templates, templates of
third-party apps and the Django admin alike, without changing any template. When the middleware is loaded it wraps
`django.forms.Media.render_css` and `render_js` once; outside a request (or without a nonce) the output is unchanged.
Only tags generated by `Media` from Python code are touched, never the response as a whole, so injected HTML never
receives the nonce.

### Per template: `csp_nonce` filter

```django
{% load pipeline_csp %}
{{ form.media|csp_nonce:request }}
{{ form.media.js|csp_nonce:request }}
```

Renders like `{{ form.media }}`, with the nonce on every `<link>` and `<script>`. Values that are not `Media`, `request=None` or
a missing nonce leave the output unchanged. `request` must exist in the context: Django resolves filter arguments
strictly, so a missing variable raises `VariableDoesNotExist`. Keep this in mind for templates that are rendered
without a request, e.g. by Django's default `server_error` view for `500.html`.

Filter and middleware can be combined; the nonce is never added twice.

## Nonce sources

The nonce is taken from, in this order:

1. Django's built-in CSP (Django >= 6.0): `django.middleware.csp.ContentSecurityPolicyMiddleware`
2. [django-csp](https://github.com/mozilla/django-csp) >= 4.0: `csp.middleware.CSPMiddleware` (`pip install django-pipeline-csp[django-csp]`)

Your policy must include the nonce in `script-src`, e.g. `CSP.NONCE` (Django) or `csp.constants.NONCE` (django-csp).
For stylesheets, add it to `style-src` as well. Note that browsers ignore `'unsafe-inline'` in `style-src` once a
nonce is present there; inline `style="..."` attributes then need `style-src-attr 'unsafe-inline'` (or no inline
styles at all). Host sources in `style-src` keep working alongside the nonce.
Without an active middleware the output is identical to django-pipeline's.

`request` must be in the template context (`django.template.context_processors.request`).

The tag must render before the CSP middleware writes the response header — normal template rendering already
satisfies this. Accessing the nonce after the header was written raises `CSPNonceError` with django-csp; with
Django's built-in CSP a late nonce is simply not included in the header and the scripts it was meant to allow are
blocked. This matters mainly for streaming responses, where content can be produced after the headers are sent.

## Not covered

- Jinja2 templates
- Inline `<style>` blocks in your own templates: use `nonce="{{ CSP_NONCE }}"` (django-csp context processor
  `csp.context_processors.nonce`) or `nonce="{{ csp_nonce }}"` (Django >= 6.0, `django.template.context_processors.csp`)
- Scripts injected via `document.write` (`'strict-dynamic'` does not trust parser-inserted scripts)
- Inline event handlers such as `onclick="..."`

## Compatibility

Python 3.10–3.14, Django 4.2 / 5.0 / 5.1 / 5.2 / 6.0 / 6.1, django-pipeline >= 4.1.

## License

MIT
