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

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

5 Likes