eagle-head

eagle-head

Proposal: mix phx.gen.csp — CSP Level 3 support for Phoenix

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:

  1. Write a custom Plug for nonce generation
  2. Figure out how to propagate the nonce to LiveView
  3. Deal with the inline <script> in the default root.html.heex (dark mode toggle)
  4. Handle CSP breaking the Plug.Debugger error page in dev
  5. 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:

  1. Create a CSP nonce Plug in the user’s project
  2. Modify router.ex, root.html.heex, and app.js
  3. 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 cookie
  • connect_params: Doesn’t touch the session, but requires JS-side changes to app.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' to script-src only 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.md and 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!

Most Liked

eagle-head

eagle-head

Update: CSP Level 3 — findings from a real implementation

Reproduction repo: eagle-head/drink_water

  • main branch — CSP Level 3 with 'unsafe-inline' in style-src (working)
  • feat/csp-level3-strict branch — strict CSP (no 'unsafe-inline') with violation reporting enabled. Run mix phx.server and open /dashboard to see violations in logs/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-src with nonce + strict-dynamic — works perfectly. All scripts execute correctly with nonce attributes. strict-dynamic propagates trust as expected.
  • Nonce propagation to LiveView — works via :session option on live_session (thanks @LostKobrakai).
  • <meta name="csp-nonce"> tag — JavaScript can read the nonce for client-side use.
  • CSP violation reporting — implemented a securitypolicyviolation event listener that sends reports to a Phoenix endpoint, since report-to / report-uri don’t work on localhost (Chrome requires HTTPS and a real domain for the Reporting API).

What breaks: style-src without 'unsafe-inline'

When I set style-src 'self' 'nonce-...' (no 'unsafe-inline'), I got 30+ violations per page load on /dashboard. All violations are style-src-attr (inline style attributes). Three distinct sources were identified.

Before diving in, here’s the key insight from the MDN style-src documentation:

“Styles properties that are set directly on the element’s style property will not be blocked, allowing users to safely manipulate styles via JavaScript.”

This means the CSP spec distinguishes between:

Method Blocked by style-src? Reference
element.style.property = "value" No — always allowed MDN: style-src
element.style.setProperty("prop", "val") No — always allowed Same DOM API as above
element.setAttribute("style", "...") Yes — blocked MDN: style-src
element.style.cssText = "..." Yes — blocked MDN: style-src
<style> tag without nonce Yes — blocked W3C CSP3 spec
style="..." attribute in HTML Yes — blocked W3C CSP3 spec

Important: a nonce on a <script> tag does not grant that script permission to use setAttribute("style") or cssText. These are controlled by style-src, not script-src. The only CSP-safe way for JavaScript to modify styles is via the DOM style API (element.style.property or element.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 the morphAttrs function during DOM patching:

// morphdom's morphAttrs function
fromNode.setAttribute(attrName, attrValue); // when attrName === "style" → CSP violation

setAttribute("style", ...) is blocked by CSP style-src without '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 strict style-src.

The fix would be to have morphdom treat style as a special case. Instead of setAttribute("style", value), parse the style string and apply property-by-property via the DOM API:

// CSP-safe approach — parse and apply per-property
function applyStyleCSPSafe(el, styleString) {
  // Remove properties no longer present
  while (el.style.length > 0) {
    el.style.removeProperty(el.style[0]);
  }
  // Parse and apply new properties
  const temp = document.createElement("div");
  temp.style.cssText = styleString; // off-DOM, no CSP violation
  for (const prop of temp.style) {
    el.style.setProperty(
      prop,
      temp.style.getPropertyValue(prop),
      temp.style.getPropertyPriority(prop),
    );
  }
}

Note: my original proposal suggested el.style.cssText = value as a fix, but MDN confirms that cssText is also blocked by CSP. The only safe path is element.style.setProperty() or direct property assignment.

2. DaisyUI components (countdown, radial-progress)

DaisyUI uses style="--value:X" as its data API for countdown and radial-progress components (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

Layer Issue Who needs to fix it Reference
morphdom (LiveView) setAttribute("style") blocked by CSP phoenix_live_view — patch morphdom to apply styles per-property via el.style.setProperty() MDN: style-src
DaisyUI style="--value:X" as component API daisyui — library-wide CSP support DaisyUI source
topbar Likely false positive from morphdom Verify after morphdom fix MDN: style-src

What 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 of setAttribute("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:

  1. Open an issue + PR on phoenix_live_view — patch morphdom’s morphAttrs to apply styles per-property via el.style.setProperty() instead of setAttribute("style"), which is the only CSP-safe approach per the W3C CSP3 spec
  2. Open an issue + PR on DaisyUI — not a workaround for specific components, but a robust, library-wide solution that enables CSP compliance for any existing or future component that relies on inline styles or JavaScript. The goal is to make CSP Level 3 a first-class concern in DaisyUI’s architecture.
  3. Write the Phoenix CSP guide — documenting the current state, the workaround ('unsafe-inline' for style-src), and how to achieve full CSP Level 3 once the morphdom fix lands

Before opening the issues/PRs, I wanted to check with the community:

  • Does the morphdom approach (el.style.setProperty() per-property instead of setAttribute("style")) seem right? Are there edge cases I’m missing?
  • Is there a reason morphdom uses setAttribute for style instead of the DOM style API?
  • Has anyone else hit this wall with CSP + LiveView?

References

LostKobrakai

LostKobrakai

You can put data on the LV session (the one inlined in the markup) with the :session option on the live_session macro. That won’t put the data on the session cookie.

steffend

steffend

Phoenix Core Team

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.

Where Next?

Popular in Proposals: Ideas Top

sbennett33
When building a component library, it is often useful to give users the ability to customize the underlying element or component to use. ...
New
markevans
Hi! I feel like Phoenix is slightly missing a trick when it comes to front-end Javascript libraries like React, Svelte, etc. I feel tha...
New
woylie
We are seeing a lot of warning logs like this: navigate event to "https://someurl" failed because you are redirecting across live_sessio...
New
mikesax
On a Rails/Turbo site, the first page is typically loaded using http GET and then sockets are used navigate and replace HTML content for ...
New
cevado
IEx is a very powerfull shell and it would be awesome to have all this power integrated inside a code editor. Clojure enables something l...
New
bartblast
This could resolve to {[a: 1, b: 2]}. Was it ever considered to allow such syntax? Notice this: {:abc, a: 1, b: 2} and this: my_fun(:abc,...
New
kccarter
This is likely a feature request unless we’re overlooking something, but it would be a nice improvement to the developer experience if th...
New
Oliver
One common problem we face in constructing lists is that there is (AFAIK) no support for conditionally inserting members into list declar...
New
dibok
Hi, I’m trying to use phoenix.js in my Qt QML project which has it’s own buildin JavaScript engine. Problem is that (what I googled so f...
New
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...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement