The hx-nonce extension gates htmx attribute processing behind CSP nonces, protecting against HTML injection attacks on sites that already use script nonces.
Installing
<script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/ext/hx-nonce.js"></script>
The hx-nonce Attribute
The hx-nonce attribute is how the server signals to the extension that an htmx element was intentionally rendered by trusted server-side code. When htmx initialises an element, the extension checks that its hx-nonce matches the page nonce — if it doesn’t match or is missing, all htmx attributes are stripped and the element is inert.
Your templating engine should stamp hx-nonce on every element that carries htmx attributes:
<!-- Django --> <button hx-post="/save" hx-nonce="{{ request.csp_nonce }}">Save</button> <!-- Rails ERB --> <button hx-post="/save" hx-nonce="<%= content_security_policy_nonce %>">Save</button> <!-- Laravel Blade --> <button hx-post="/save" hx-nonce="{{ csp_nonce() }}">Save</button>
Important: hx-nonce alone does not guarantee XSS protection. It proves the element was rendered by the server, but it says nothing about the safety of the attribute values on that element. Any htmx attribute that includes user-supplied data must still be properly escaped by your templating engine:
<!-- UNSAFE — user data in hx-get without escaping --> <div hx-get="/search?q={{ user_input }}" hx-nonce="...">...</div> <!-- SAFE — escaped by the templating engine --> <div hx-get="/search?q={{ user_input | escape }}" hx-nonce="...">...</div>
All major templating engines escape HTML entities (", ', <, >, &) by default — make sure you are not bypassing this with raw/unescaped output filters (| raw, | safe, raw() etc.), which would allow a " in user input to break out of the attribute and inject new attributes.
URL-valued attributes (hx-get, hx-post, etc.) are also blocked by the extension if the value is a js: or javascript: URI — these schemes survive entity encoding and are not valid action URLs.
Set a Content-Security-Policy header with a per-request nonce and stamp hx-nonce on every htmx element:
Content-Security-Policy: script-src 'self' 'nonce-<nonce>'
<script nonce="<nonce>" src="/htmx.js"></script> <script nonce="<nonce>" src="/hx-nonce.js"></script> <button hx-post="/save" hx-nonce="<nonce>">Save</button>
The extension reads the page nonce from the first script[nonce] element and blocks any htmx element whose hx-nonce doesn’t match. If no page nonce is found, all htmx processing is blocked — fail closed.
Trusted Types
Loading hx-nonce automatically creates a 'htmx' Trusted Types policy. Add it to your CSP to enforce that only htmx can write HTML into DOM sinks:
Content-Security-Policy: script-src 'self' 'nonce-<nonce>'; require-trusted-types-for 'script'; trusted-types htmx
No extra configuration needed. Falls back transparently on browsers without Trusted Types support. If 'htmx' is not in your trusted-types whitelist, all htmx processing is blocked — fail closed.
Safe Eval
htmx’s JS expression features (hx-on:, hx-vals js:, hx-confirm js:, trigger filters) are entirely optional. If you don’t use them, you can omit unsafe-eval from your CSP entirely with no further configuration.
If you do use these features, set safeEval:true to replace htmx’s new Function() eval with nonce-based script injection, enabling them without unsafe-eval:
Content-Security-Policy: script-src 'self' 'nonce-<nonce>'
<meta name="htmx-config" content='extensions:"hx-nonce",safeEval:true'>
Partial Responses
Partial responses should include a Content-Security-Policy header with a fresh nonce. The extension rewrites the response nonce to the page nonce before fragment parsing so swapped-in elements pass subsequent nonce checks:
Content-Security-Policy: script-src 'nonce-<response-nonce>'
Nonce Reuse Protection
The extension rewrites the response nonce to the page nonce so swapped-in elements pass subsequent nonce checks. Before doing that rewrite, it scrubs any element that already carries the page nonce value from the raw response text.
The server cannot know the page nonce — it only knows its own per-response nonce. So if the page nonce appears in a response, it was put there by an attacker, not the server. Scrubbing it first means the rewrite pass cannot accidentally promote attacker-controlled elements to trusted status.
The risk: unlike <script nonce>, hx-nonce attributes are not blanked by browsers after parse, so they are a possible additional nonce exposure surface. The scrub step is a defence-in-depth measure to ensure a stolen nonce cannot be pre-stamped into injected content to pass nonce checks.
Inline Scripts in Swapped Content
When htmx swaps in HTML containing <script> tags, it re-creates them to trigger execution. The hx-nonce extension ensures the response nonce is rewritten to the page nonce before parsing, so script nonces are correctly promoted and execute under a strict script-src 'nonce-<nonce>' policy.
This is the ideal replacement for htmx.config.inlineScriptNonce. Do not use inlineScriptNonce when using this extension — it applies a single static nonce to all swapped scripts regardless of origin, which undermines the per-response nonce model this extension provides.
Security Events
| Event | Fired when |
|---|---|
htmx:security:strip | Element stripped due to missing or mismatched nonce |
htmx:security:violation | Nonce mismatch at eval time, unnonced boosted form submitter, or js:/javascript: action URL |
document.addEventListener('htmx:security:strip', e => { console.warn(e.target, e.detail.reason, e.detail.stripped); });
detail.reason is 'missing-nonce', 'nonce-mismatch', or 'javascript-url'.