A per-request policy build (forced by the nonce) means a stray semicolon in
a CSP_EXTRA_* value — a plausible operator paste like
"https://youtube.com; https://vimeo.com" — lands inside a single source
token and makes Rails raise InvalidDirectiveError on every request, a
site-wide 500 even in report-only mode. Split on ';' alongside comma and
whitespace so such a value tokenizes into valid sources. Rails still
validates each token, so no injection is introduced (a token with an
embedded space still fails validation).
Campfire is self-hosted per customer: an admin may embed or connect to
external hosts (video/embed providers, image CDNs, analytics, form or
webhook endpoints) that vary per install and are unknown at build time.
Hardcoding these directives at :self would break those integrations once
the policy is enforced.
Read per-install extras from ENV (CSP_EXTRA_SCRIPT_SRC, _STYLE_SRC,
_IMG_SRC, _CONNECT_SRC, _FRAME_SRC, _FORM_ACTION), each a comma- or
whitespace-separated host list, appended to the :self baseline. Unset by
default, so the policy stays at :self only unless an admin opts in.
Layer a Turbo-safe nonce and a full directive set onto the Report-Only
baseline, still without enforcing anything.
The nonce is the HMAC-SHA256 of a stable per-visitor cookie, keyed by
secret_key_base. It stays constant across a session's requests so Turbo
snapshot restores don't replay a stale nonce and trip the policy, while
staying unpredictable to a client that can set the cookie but not the
secret. importmap-rails auto-nonces the importmap JSON and shim, the only
inline scripts the app renders (the message _template is a non-executed
text/template data block).
script-src gains the nonce; style-src keeps unsafe_inline for now (inline
style= attributes across the composer and ActionText content aren't
nonceable yet); img/connect/frame-src start at :self as tuning starting
points for the report-only window. report_only stays true; enforcement is
a later flip.