Hi everyone,
I’ve been researching Content Security Policy Level 3 support in Phoenix and wanted to share my findings and a proposal for discussion. I’ve read through several forum threads on the topic (nonce as boolean, passing nonce to LiveView, CSP breaking error pages, dynamic styles with CSP) and Dan Schultzer’s excellent blog post on CSP with LiveView. It’s clear the community has been working around this manually for a while.
The current state
Phoenix 1.8 took a great first step — put_secure_browser_headers now sets base-uri 'self'; frame-ancestors 'self'; by default (thanks to chrismccord and SteffenDE). But there’s still no built-in path to a strict CSP with nonces and strict-dynamic.
Developers who want this today need to:
- Write a custom Plug for nonce generation
- Figure out how to propagate the nonce to LiveView
- Deal with the inline
<script>in the defaultroot.html.heex(dark mode toggle) - Handle CSP breaking the Plug.Debugger error page in dev
- Navigate browser quirks (Chrome hides nonce values in the DOM inspector, which is intentional but confusing)
A possible direction: mix phx.gen.csp
I’m exploring the idea of a generator — similar to phx.gen.auth — that would set up CSP Level 3 support. The generator would:
- Create a CSP nonce Plug in the user’s project
- Modify
router.ex,root.html.heex, andapp.js - Add a
<meta name="csp-nonce">tag for JS access
The code would live entirely in the user’s project (not in Phoenix core), so it’s fully customizable.
Two tiers of support (proposed)
Tier 1 — Complete (LiveView ± DaisyUI): Everything works out of the box after running the generator.
Tier 2 — Base + docs (API, SPAs, external JS libs): The generator sets up the foundation. Documentation provides guidance for adapting to specific stacks.
Default policy (proposed, in report-only mode)
default-src 'self';
script-src 'nonce-{nonce}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self' ws: wss:;
object-src 'none';
form-action 'self';
base-uri 'self';
frame-ancestors 'self'
CSP violation reporting (proposed)
A pluggable reporting architecture using a behaviour — the controller receives reports and delegates to a configurable handler. Users could plug in Logger, Ecto, Grafana/Loki, Datadog, Sentry, or any custom backend. This would be documented as boilerplate, not auto-generated.
Open questions — I’d love your input
These are the design decisions I’m not sure about. I have opinions but I’d rather hear from the community first:
1. Nonce propagation to LiveView
The community has converged on put_session + on_mount (as described in Dan Schultzer’s post). Another option is connect_params via the LiveSocket JS constructor. Each has trade-offs:
put_session+on_mount: Works reliably, but adds data to the session cookieconnect_params: Doesn’t touch the session, but requires JS-side changes toapp.js
Which approach do you prefer? Is there a third option I’m missing?
2. Development mode
CSP strict breaks Plug.Debugger’s error page (reported here). Options:
- Disable CSP in dev — simplest, but you lose dev/prod parity
- Relaxed CSP in dev — add
'unsafe-inline'toscript-srconly in dev - Fix Plug.Debugger — add nonce support to the debugger itself (separate PR to Plug)
What would you expect as default behavior?
3. Generator vs standalone library
Should this be:
- Part of Phoenix (
mix phx.gen.csp) — discoverable, maintained with the framework - Standalone hex package (e.g.,
phx_csp) — independent release cycle, less pressure on the core team - Just documentation — add a comprehensive guide to
guides/security.mdand let developers copy/paste
4. Default enforcement mode
Should the generated policy start as:
report-only— safe rollout, but users might forget to switch to enforcement- Enforcing — secure by default, but might break things if the user has inline scripts from third-party libs
5. The inline dark mode script
The default root.html.heex has an inline <script> for dark mode. With nonce-based CSP, options are:
- Keep inline + add nonce — zero FOUC, CSP compliant
- Move to external file — simpler CSP but risks FOUC
- Leave as-is — the generator adds the nonce attribute
Is there a preference?
6. Interaction with put_secure_browser_headers
The existing put_secure_browser_headers sets a basic CSP. The nonce plug would need to either replace or extend it. When both headers coexist (content-security-policy + content-security-policy-report-only), browsers apply both. When migrating to enforcement, the nonce plug would replace the default CSP.
Is this interaction clear enough, or should the generator modify put_secure_browser_headers directly?
What I have so far
I’ve written a detailed design spec covering architecture, data flow, error handling, testing, and migration for existing projects. I’m happy to share it if there’s interest.
I also have a JS library compatibility analysis:
| Library | Works? | Notes |
|---|---|---|
| LiveView | Yes | Full Tier 1 support |
| DaisyUI | Yes | CSS only, no JS issues |
| Stimulus | Yes | No inline scripts |
| Alpine.js | Partial | Needs 'unsafe-eval' |
| Flowbite (inline handlers) | Partial | Needs 'unsafe-hashes' |
| React / Vue / Svelte (SPA) | Yes | Modern frameworks avoid eval |
| Google Analytics / GTM | Yes | 'strict-dynamic' propagates trust |
Next steps
Depending on the feedback here, I’d be happy to:
- Share the full design spec for review
- Submit a PR (to Phoenix or as a standalone package)
- Start with just the documentation/guide if that’s preferred
Looking forward to hearing your thoughts — especially on the open questions above. Any experience with CSP in production Phoenix apps would be incredibly valuable!






















