Django: introducing django-integrity-policy

Back in January, Firefox’s Security & Privacy Newsletter for 2025 Q4 piqued my interest with this mention:
Integrity-Policy: Firefox 145 has added support for the Integrity-Policy response header. The header allows websites to ensure that only scripts with an integrity attribute will load.
A new security header! That’s right up my street: I’ve cared about getting security headers right since 2018, when I created django-permissions-policy to set the Permissions-Policy header. (At the time, it was called Feature-Policy: why they changed it, I can’t say, people just liked it better that way.)
The new Integrity-Policy header helps with subresource integrity, a tool for securely including third-party scripts and stylesheets on your website. Browsers support the integrity attribute on <script> and <link> tags, which allows you to specify a hash of the expected content, like:
<script
src=https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta4/dist/htmx.min.js
integrity=sha384-aWZK1NtOs/aWb/+YZdTM8q2JkWEshlMc9mgZ189numT9bwFhyAyYEoO4nO/2dTXt
crossorigin=anonymous></script>
If the content downloaded from the external source doesn’t match the expected hash, the browser blocks it from loading. This is a great defense against the target URL changing its contents, executing a supply chain attack against your visitors.
(Generally, I recommend you avoid loading anything from a third-party URL, per Reasons to avoid Javascript CDNs. But sometimes, you gotta do what you gotta do, and declaring integrity is a great idea, then.)
Integrity-Policy allows you to opt in to requiring integrity attributes on your page, ensuring that you can never load potentially-compromised resources. The header is fairly simple, at least right now—here’s a complete example that requires integrity for all scripts and stylesheets:
Integrity-Policy: blocked-destinations=(script style)
You can add in endpoints to tell browsers where to send violation reports to, and there’s the second Integrity-Policy-Report-Only header which lets you test a policy without enforcing it.
Note there’s no possibility to differentiate between first- and third-party resources in Integrity-Policy. If you set it, you’ll need to add integrity attributes to all your scripts and stylesheets, including those you host yourself. This is by design: the header is being developed as part of Web Application Integrity, Consistency and Transparency (WAICT), an initiative to bring app-store-level “code signing” to the web, where users can be sure that CDNs and other intermediaries haven’t tampered with served code.
Integrity-Policy is supported on Firefox 145+ and Chrome 138+.
django-integrity-policy
My new package, django-integrity-policy provides a middleware for setting the Integrity-Policy headers in a familiar Django-style way. You install it, add the middleware:
MIDDLEWARE = [
...,
"django.middleware.security.SecurityMiddleware",
"django_integrity_policy.IntegrityPolicyMiddleware",
...,
]
…and then configure the appropriate setting(s):
INTEGRITY_POLICY = {
"blocked-destinations": ["script", "style"],
}
So far, it’s pretty basic, but I expect WAICT will increase the complexity of Integrity-Policy over time, and I’ll add support for new options as they come along.
Once Integrity-Policy is set, the browser will block any scripts or stylesheets (depending on configuration) that lack a valid integrity attribute, including your first-party resources. That means you need to add integrity attributes to all your static files. Luckily, this has been considered before by the legendary Jake Howard, who made a package called django-sri. It provides template tags to generate appropriately hashed HTML tags. For example:
{% load sri %}
{% sri_static "app.js" %}
{% sri_static "app.css" %}
…will output:
<script src="/static/app.js" integrity="sha256-..."></script>
<link rel="stylesheet" href="/static/app.css" integrity="sha256-..."/>
These tags would be allowed under a maximally strict integrity policy.
See the example application in the django-integrity-policy repository for a full working project.
LLM generation
I built this package using just two prompts to Claude. I copied the repository for my previous security header package, django-permissions-policy, and reset its Git history. I then used this prompt to Claude, inside Zed:
The current repository is a copy of my package django-permissions-policy
It's time to turn it into django-integrity-policy, for the relevant headers per the below mdn docs
(Integrity-Policy MDN docs from https://github.com/mdn/content/blob/main/files/en-us/web/http/reference/headers/integrity-policy/index.md?plain=1)
(Integrity-Policy-Report-Only MDN docs from https://github.com/mdn/content/blob/main/files/en-us/web/http/reference/headers/integrity-policy-report-only/index.md?plain=1)
Check and edit every file to be that new package, copyright 2026, keeping the general testing infrastructure and so on.
Don't run any commands yet, just check and edit every file
I then took a 20-minute nap and woke up to a near-complete package. I reviewed it, ran the tests, made some minor edits, committed, and pushed to PyPI!
LLMs are rightfully a hot topic, with heady supporters and heavy detractors. I use them begrudgingly and somewhat sparingly, and I cannot wait for the future where I can stick to local models. (Gemma 4 can run on my M1 Mac and approaches Claude’s performance on many tasks, so we’re getting there.)
This task, though, was a perfect fit for LLM code generation: the existing repository acted as great context for structure, the new package was very similar in shape (“same same but different”), and the documentation provided a clear specification for what to build. The LLM could mash things up for me with minimal oversight, and I could check the work quickly.
I would guess that overall, using an LLM saved me a couple of hours of mostly grunt-work, like checking every copied-over configuration file. That’s pretty valuable for me, and honestly made creating the package feasible.
The future
I’m not sure how widely used Integrity-Policy will be, and therefore how popular django-integrity-policy will end up. But this was an interesting exercise and I am interested to see how the header and other work from WAICT evolves. I will try to keep the package updated, and we’ll see if it ever reaches a point where proposing support in Django itself makes sense.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- How to Score A+ for Security Headers on Your Django Website
- How to set the new COEP, COOP, and CORP security headers in Django
Tags: django