The response headers that are the same whatever was asked for, and the Content-Security-Policy, which is not.
Two audiences, two policies
The admin and the sites it publishes are the same application on the same origin, so the policy
cannot be chosen by pipeline — every browser route goes through :browser. It is chosen by path.
A PUBLIC page is nearly strict already: measured, a database-backed page carries one inline
<script> (the theme bootstrap, which now takes a nonce) and no inline style at all.
The ADMIN cannot be, and the reason is the page builder. vendor/tailwind-browser.js is Tailwind
compiled to run in the browser: the reader types px-7 into the inspector and the utility has to
exist a frame later, which it achieves by creating a <style> element — precisely what
style-src governs, from a library that offers no nonce. An editor that generates CSS at runtime
and a strict style-src cannot both be had.
Report-only first
:report_only is the default because a policy is a guess until the browser has checked it against
real pages. It sends Content-Security-Policy-Report-Only, which reports violations and blocks
nothing. Move to :enforce once the reports are quiet:
config :mishka_cms, MishkaCmsWeb.Runtime.Plugs.SecurityHeaders, csp: :enforce # or :report_only, or :offx-frame-options was set to DENY, and the builder is an iframe
DENY forbids framing by ANY page, including one from the same origin — and the editor's canvas is
a same-origin iframe of /admin/dashboard/runtime/builder-canvas/…. It used to work in development
only by accident: tidewave (then a only: [:dev] dependency, plugged into the endpoint) deleted
the header in a before_send callback so its own iframe survived, and took the editor's along with
it. In production nothing deleted it. That dependency is gone, which would have returned the bug
had this header still said DENY.
So: SAMEORIGIN, and frame-ancestors 'self' for browsers that read CSP instead — which is all
of them, and which is why the header is kept only for the ones that do not.
Summary
Functions
Whether every entry of origins is an origin security.frame_origins may hold:
https://host or https://host:port, lower case, nothing after it.
Whether a style ATTRIBUTE is permitted — the one CSP question an author can walk into.
The nonce for this request, for a template that has to mark an inline <script> as ours.
Functions
Whether every entry of origins is an origin security.frame_origins may hold:
https://host or https://host:port, lower case, nothing after it.
@spec inline_style_allowed?() :: boolean()
Whether a style ATTRIBUTE is permitted — the one CSP question an author can walk into.
A nonce vouches for a <style> ELEMENT and there is no equivalent for an attribute, so a policy
can only permit every one or refuse every one. The page builder lets an author put style on
their own content, which makes refusing them a decision about what this CMS offers rather than
housekeeping: the attribute does not error, it is simply ignored, and the author is left looking
at a colour that did not apply.
So the editor reads this back and says so beside the field. One switch drives the header and the warning together, because a warning that can disagree with the policy is worse than none:
config :mishka_cms, MishkaCmsWeb.Runtime.Plugs.SecurityHeaders, inline_style: :forbid # or :allow:allow is the default, and is what a CMS whose editor offers the attribute should keep.
@spec nonce(Plug.Conn.t() | map() | nil) :: String.t() | nil
The nonce for this request, for a template that has to mark an inline <script> as ours.
Reads the assign the plug set. nil when the plug did not run — a template must degrade to no
attribute rather than to the string "nil", which would match nothing and block the script.