eagle-head
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!
Trending in Proposals: Ideas
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










Showing Posts 1 to 10- Show Best Posts
- Show All Posts (oldest first)
- Show All Posts (newest first)
LostKobrakai
You can put data on the LV session (the one inlined in the markup) with the
:sessionoption on the live_session macro. That won’t put the data on the session cookie.eagle-head
Thanks @LostKobrakai! That’s a great point — using
:sessiononlive_sessionis cleaner than bothput_session(avoids cookie bloat) andconnect_params(no JS changes needed). I hadn’t considered it and it seems like the best of both worlds for nonce propagation.I’ll update the design spec to include this as the recommended approach. Appreciate the input!
steffend
If we make it part of Phoenix, I think it should be part of the documentation. Phoenix generators mostly generate new files or assume a fresh project without major modifications. Adding CSP is useful for any project, so a generator that bails out as soon as something isn’t configured as the default phx.new project is probably not worth the effort? Of course, an igniter installer that does the default setup would be nice too, but that’s probably best explored in a separate package first.
I’m not an expert on CSP, but I feel like the default should be enforcing and also used in development. So if we need any changes, like for Plug.Debugger, we should implement those. The docs can then also contain a section of report only mode and how one could implement that.
eagle-head
Thanks @steffend, this is really helpful direction from the core team!
I agree with your points:
Documentation over generator — Makes total sense. A generator that bails on customized projects defeats the purpose, especially since CSP is most needed by existing projects. A comprehensive section in
guides/security.mdwith clear examples for LiveView, controllers, and API scenarios would serve the community much better.Enforcing by default — You’re right. Security should be opt-out, not opt-in. If the docs guide people toward enforcing from the start, that’s a stronger default posture. Report-only can be documented as an optional migration strategy for teams adopting CSP on existing production apps.
Fix Plug.Debugger — Agreed. If CSP strict breaks the error page, the right fix is in Plug.Debugger, not relaxing CSP in dev. I’d be happy to look into a separate PR for that.
So the plan would be:
guides/security.mdcovering nonce setup, LiveView integration (using:sessiononlive_sessionper @LostKobrakai’s suggestion), enforcement, library compatibility, and violation reportingI’ll start with the documentation PR. Thanks again for the guidance!
eagle-head
Update: CSP Level 3 — findings from a real implementation
Reproduction repo: eagle-head/drink_water
mainbranch — CSP Level 3 with'unsafe-inline'instyle-src(working)feat/csp-level3-strictbranch — strict CSP (no'unsafe-inline') with violation reporting enabled. Runmix phx.serverand open/dashboardto see violations inlogs/csp_violations.log.Following the feedback from @steffend and @LostKobrakai, I implemented CSP Level 3 (enforcing, nonce +
strict-dynamic) on this project and collected CSP violation reports. Here’s what I found.What works with CSP Level 3
script-srcwith nonce +strict-dynamic— works perfectly. All scripts execute correctly with nonce attributes.strict-dynamicpropagates trust as expected.:sessionoption onlive_session(thanks @LostKobrakai).<meta name="csp-nonce">tag — JavaScript can read the nonce for client-side use.securitypolicyviolationevent listener that sends reports to a Phoenix endpoint, sincereport-to/report-uridon’t work on localhost (Chrome requires HTTPS and a real domain for the Reporting API).What breaks:
style-srcwithout'unsafe-inline'When I set
style-src 'self' 'nonce-...'(no'unsafe-inline'), I got 30+ violations per page load on/dashboard. All violations arestyle-src-attr(inline style attributes). Three distinct sources were identified.Before diving in, here’s the key insight from the MDN
style-srcdocumentation:This means the CSP spec distinguishes between:
style-src?element.style.property = "value"element.style.setProperty("prop", "val")element.setAttribute("style", "...")element.style.cssText = "..."<style>tag without noncestyle="..."attribute in HTMLImportant: a nonce on a
<script>tag does not grant that script permission to usesetAttribute("style")orcssText. These are controlled bystyle-src, notscript-src. The only CSP-safe way for JavaScript to modify styles is via the DOM style API (element.style.propertyorelement.style.setProperty()). See MDN: style-src and W3C CSP3 §8.3.1. morphdom in Phoenix LiveView (critical blocker)
LiveView’s morphdom uses
setAttribute("style", ...)in themorphAttrsfunction during DOM patching:setAttribute("style", ...)is blocked by CSPstyle-srcwithout'unsafe-inline'(MDN reference). This is inherent to how morphdom works — every server-sent patch that changes a style attribute triggers a violation. This means LiveView itself is incompatible with strictstyle-src.The fix would be to have morphdom treat
styleas a special case. Instead ofsetAttribute("style", value), parse the style string and apply property-by-property via the DOM API:Note: my original proposal suggested
el.style.cssText = valueas a fix, but MDN confirms thatcssTextis also blocked by CSP. The only safe path iselement.style.setProperty()or direct property assignment.2. DaisyUI components (countdown, radial-progress)
DaisyUI uses
style="--value:X"as its data API forcountdownandradial-progresscomponents (DaisyUI radial-progress docs, DaisyUI countdown docs). No alternative (data-*attributes, CSS classes) is provided. This is a DaisyUI issue, not Phoenix.I investigated the DaisyUI source code — only these 2 components out of 50+ require user-provided
style=attributes. All other components use CSS classes to set custom properties internally.3. topbar.js
Uses
.style.property = value(direct property assignment). Per the MDN documentation, this should NOT be blocked by CSP. The violations reported from topbar are likely caused by morphdom re-applying the style attribute after DOM patches, not by topbar itself.Summary
setAttribute("style")blocked by CSPel.style.setProperty()style="--value:X"as component APIWhat this means for the CSP documentation
As @steffend suggested, the default should be enforcing. Based on these findings, the documentation should be honest:
script-src: CSP Level 3 works perfectly with nonce +strict-dynamic. No caveats.style-src:'unsafe-inline'is currently required when using LiveView, due to morphdom’s use ofsetAttribute("style")(MDN confirms this is blocked). This is a known limitation, not a design choice.I’d like to contribute fixes
I’m willing to:
phoenix_live_view— patch morphdom’smorphAttrsto apply styles per-property viael.style.setProperty()instead ofsetAttribute("style"), which is the only CSP-safe approach per the W3C CSP3 spec'unsafe-inline'for style-src), and how to achieve full CSP Level 3 once the morphdom fix landsBefore opening the issues/PRs, I wanted to check with the community:
el.style.setProperty()per-property instead ofsetAttribute("style")) seem right? Are there edge cases I’m missing?setAttributefor style instead of the DOM style API?References
styleattributes specifically<style>elements and stylesheetsreport-todoesn’t work on localhostvar(--value)set viastyle=attributeeagle-head
Update: opened a DaisyUI issue for CSP compliance
Following the findings shared in my previous post, I opened an issue on the DaisyUI repository:
saadeghi/daisyui#4475 — countdown and radial-progress break Content Security Policy
The issue proposes a
data-valueattribute with a small JS bridge as a CSP-safe alternative to the currentstyle="--value:X"API. The existing API would remain for backward compatibility.A pure-CSS solution via
attr(data-value type(<number>))(CSS Values Level 5) would be ideal, but browser support is Chrome-only today (133+). Firefox has it in Interop 2026, Safari has no known timeline.Only 2 of 58 DaisyUI components are affected — the rest are already CSP-compatible.
steffend
morphdom patches any attribute. So this is only a problem if you have inline styles in your LiveView template. The workaround is interesting though, so a PR to morphdom that explores it sounds good to me.
eagle-head
@steffend Following up on your feedback — I’ve submitted a PR to morphdom upstream:
Issue: patrick-steele-idem/morphdom#287
PR: patrick-steele-idem/morphdom#288
The fix adds a
syncStyle()function tomorphAttrs.jsthat replacessetAttribute("style", ...)with the DOM style API (style.setProperty/style.removeProperty). Per the W3C CSP3 spec, the DOM style API is explicitly exempt from CSP restrictions — so this eliminates thestyle-srcviolation without any workaround.How it works:
Compares
fromNode.style.cssTextvstoNode.style.cssTextas a fast-path (skip if unchanged)Removes properties no longer in the target via
removeProperty()Copies properties from the target via
setProperty(name, value, priority), only when the value or priority actually differsHandles standard properties, shorthands, CSS custom variables (
--value,--size), and!importantpriority. Non-style attributes are unchanged — still usesetAttributeas before.This would unblock strict
style-srcfor LiveView once morphdom releases a new version with this fix.eagle-head
@steffend and @LostKobrakai I’d like to broaden the CSP discussion to something I think is related and worth reflecting on: a roadmap toward making Phoenix CSS-agnostic.
I’m not suggesting we rip out Tailwind or DaisyUI overnight — they serve their purpose well for quick starts. But I believe the project should gradually move toward being framework-agnostic on the CSS/component side. Here’s why:
The security argument
Once we implement CSP support in Phoenix (which this thread is about), the default template must be CSP-compatible out of the box. Today, DaisyUI has 2 components that require
'unsafe-inline'instyle-src(daisyui#4475), and the initial response from their core contributors has been dismissive — a thumbs-down with no comment. We can’t guarantee that a third-party CSS library will align with Phoenix’s security goals.The ecosystem has changed
When DaisyUI was adopted (March 2025), it was one of the few CSS-only component libraries that checked all the boxes José outlined in #6121. But the landscape has evolved rapidly:
Evidence from the community
The data suggests the community is ready for this:
fieldset-label, breaking Phoenix’s CoreComponents — a maintenance cost we absorb from a dependency we don’t controldarkvariant with system mode is still broken and openWhat I’m proposing (not a revolution, an evolution)
A gradual roadmap, not a breaking change:
phx.new— similar to what @zachdaniel is exploring with Igniter, or the wizard-style installers he mentioned in #6121DaisyUI can absolutely remain the default for quick starts. But the architecture should make it a choice, not a coupling. This aligns with Phoenix’s philosophy of being explicit and transparent — the same principles that make the framework great.
What do you think? Is this something worth exploring as part of the broader CSP effort, or should it be a separate discussion?
steffend
I’m not sure that I necessarily agree with this. Strict CSP has benefits for sure, but it’s still an advanced topic that one might not want to enforce in a beginner friendly, out of the box, experience (phx.new). So assuming this proposal ends up as a comprehensive guide about CSP in the docs, there’s no direct link to phx.new adhering to that.
I would not mix daisyUI into this discussion. In fact, I’d argue that Phoenix is CSS-agnostic. The installer is a starting point for new applications that we maintain. Phoenix - the framework - does not make any assumptions about what kind of HTML your template ends up rendering and what CSS or JS libraries are involved in that. You can use vite instead of esbuild, you can use any of the libraries you mentioned instead of daisy.
The big problem with any change to the generators is that it necessarily increases the maintenance burden on the very small Phoenix team. Providing unstyled components either means having two variants to maintain, or defining a strict API for the generators to ensure that third party libraries don’t break when we need to do changes between Phoenix releases, which limits us in what we can change. (That problem already exists today, but by not defining that API we’re not restricting what we can do, putting the burden on the libraries to keep up - which of course is not optimal.).
If you can come up with a maintainable pluggable CSS system, I’d be happy to hear about it, but I currently don’t see a way towards that that does not require a very significant amount of work.
So tl;dr: let’s keep the CSS discussion separate from CSP.