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

c4710n
This is an idea for improving the workflow of iex -S mix phx.server The Problem and The Solution in use When running multiple Phoenix en...
New
derekkraan
I have been using a multi-endpoint setup in my app, just because I think it makes the most sense for a multi-subdomain app. This worked w...
New
sevensidedmarble
I have a very simple suggestion: the generated config/dev.exs file should read from PORT at compile time to set the endpoints port. If ...
New
superchris
Hello! After seeing a couple of posts from Chris and Jose about supporting web components in LiveView, I thought it would be great to ini...
New
superchris
Currently there is no out of the box way to support DOM Custom Events in phoenix. I’ve created a separate library to do this, but I’d lov...
New
dvartic
Would there be interest to implement a link/1 that gives to option to operate on other events? So I implemented a simple link/1 function...
New
victor23k
Hi all! I come with a proposal to create LiveViews inside the shadow DOM. At Doofinder, we have experience building embeddable LiveView a...
New
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
andreamancuso
Hey folks, This might sound niche, but I think it’s worth bringing up - especially given Phoenix’s reputation for being lightweight, por...
New
pinetops
LiveView is by far my favorite web tech, but a few things have been nagging me. So with all the fancy and ill advised elixir tricks I cou...
New

Other popular topics Top

JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
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
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43757 214
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement