MishkaCmsCore.Runtime.CspNonce (Mishka CMS Core v0.0.1)

Copy Markdown View Source

The Content-Security-Policy nonce, carried from the plug into a LiveView and into every template it renders.

Why a LiveView needs it at all

The root layout can read conn.assigns and so marks its inline <script> without help. A LiveView's OWN template cannot: it renders from its socket, which never saw the conn. Several templates here emit a <style> element — the editor's canvas draws its chrome, its icons and the site's theme that way — and every one of them is refused by style-src-elem without a nonce.

One nonce per response, signed into the LiveView session

MishkaCmsWeb.Runtime.Plugs.SecurityHeaders mints a fresh nonce for every response, as the CSP specification asks: a nonce is only worth something while it cannot be guessed, and one that outlives its response is one an attacker has longer to learn.

A LiveView keeps rendering over its socket long after that response, and everything it renders must carry the nonce THAT document's header named. The socket's own session is the cookie at the moment it connects, so a nonce kept there would follow whichever tab loaded last. Instead, session/1 is a live_session :session function: Phoenix signs what it returns into the page (data-phx-session), the connected mount reads it back from that token, and a live navigation inside the session carries the same token — so every render of one document agrees with its own header, and two tabs never share a nonce.

live_session :public, session: {MishkaCmsCore.Runtime.CspNonce, :session, []} do

Using it in a template

@csp_nonce in a LiveView's own template, and csp_nonce() anywhere a runtime template is compiled — a page, a layout, a component — because a component sees only the attrs it is handed:

<style nonce={csp_nonce()}>…</style>

A <style> is the last resort. A class, the element's CSS box in the editor or a stylesheet row is cached, shared between pages and needs no nonce at all.

on_mount/4 records the value for the process, and the plug does the same for a response that renders no LiveView, so current/0 is one process-dictionary read on every call.

Summary

Functions

The nonce of the response this process is rendering, or nil outside one.

The process dictionary key put/1 writes, which MishkaCmsCore.Runtime.LiveViewHelpers.carry_into_tasks/0 registers so a task a template starts renders with the same nonce.

Records nonce as the one this process renders with. nil clears it.

What a live_session signs into the page for its LiveViews: this response's nonce.

The session key session/1 writes and on_mount/4 reads.

Functions

current()

@spec current() :: String.t() | nil

The nonce of the response this process is rendering, or nil outside one.

Set by the plug for the request process and by on_mount/4 for a LiveView process.

process_key()

@spec process_key() :: term()

The process dictionary key put/1 writes, which MishkaCmsCore.Runtime.LiveViewHelpers.carry_into_tasks/0 registers so a task a template starts renders with the same nonce.

put(nonce)

@spec put(String.t() | nil) :: :ok

Records nonce as the one this process renders with. nil clears it.

session(conn)

@spec session(Plug.Conn.t()) :: %{optional(String.t()) => String.t()}

What a live_session signs into the page for its LiveViews: this response's nonce.

%{} when the plug did not run, so the session carries nothing rather than a nil the mount would have to tell apart from a real value.

session_key()

@spec session_key() :: String.t()

The session key session/1 writes and on_mount/4 reads.