Add opt-in CSP nonce support for scripts - #20301
m-develops wants to merge 7 commits into
Conversation
Adds a `FilamentCsp` manager and facade in the support package, so that
applications sending a Content Security Policy can have Filament render a
`nonce` attribute on the scripts it outputs, removing the need for
`'unsafe-inline'` in `script-src`.
FilamentCsp::useNonce(static fn (): string => csp_nonce());
The nonce is resolved once per request, so every element in a response
receives the same value. `\Filament\Support\csp_nonce()` returns the raw
value and `\Filament\Support\csp_nonce_html()` returns the whole attribute,
for use in application and plugin templates.
The manager lives in the support package rather than on the panels
`Filament` facade because assets render without a panel, and CSP is not
purely asset related.
Styles are deliberately untouched: Livewire, Alpine and TipTap all rely on
inline styles, so `style-src 'unsafe-inline'` is still required.
Until a nonce is configured the rendered output is unchanged, byte for byte.
835bf1f to
67c4b89
Compare
Co-authored-by: Amp <amp@ampcode.com>
|
@m-develops I’ve pushed some changes to use Laravel’s existing Vite nonce instead of introducing a separate Filament API. It seems like Livewire already reads the nonce from Vite, so this keeps Filament and Livewire on the same value. This also works for apps using Mix, since the Vite facade is only being used to store the nonce. I’ve updated the tests and docs to match. What do you think of this approach? |
Co-authored-by: Amp <amp@ampcode.com> # Conflicts: # docs/09-advanced/02-assets.md
Clarified information about Vite facade and frontend tooling compatibility. Signed-off-by: Dan Harrin <git@danharrin.com>
|
Agreed this is better. One question: the last docs commit removed the aside about SPA mode needing Livewire 4.2.2 or later. Earlier versions do not preserve the active document's nonce during Happy to rebase the follow-up phases (inline styles, then Alpine's |
|
I just find that those docs refs to specific versions end up becoming out of date, and users debug by updating dependencies anyway, so I don't think it's worth mentioning. We don't even mention what versions of Filament support this feature anyway. |
|
@danharrin one thing from running your changes locally on The inline data script is no longer byte identical when no nonce is configured.
<script > <!-- no nonce configured -->
<script nonce="abc123" > <!-- nonce configured -->against <script{{ filled($nonce) ? new Illuminate\Support\HtmlString(' nonce="' . e($nonce) . '"') : '' }}>Your reply on the SPA aside crossed with this comment. Understood on not pinning versions in the docs, dropping it. For what it is worth: |
|
Correction on the snippet above: it does not survive |
|
@danharrin ignore the format-ignore suggestion above, there is a better answer and it is already in Filament.
@php
$cspNonce = Vite::cspNonce();
$cspNonceAttribute = filled($cspNonce) ? new HtmlString(' nonce="' . e($cspNonce) . '"') : '';
@endphp
<script @trim {{ $cspNonceAttribute }}>Your While testing this I found the follow-up work needs the same tag in six more places, and that raised a question worth asking now rather than in the next PR. Would you take a <script @trim @filamentNonce>The case for it: the The case against: you have just deleted Filament's CSP API on purpose, and this is Filament API for CSP again, even if it is only view sugar resolving to I lean towards adding it, but it is your call and the follow-up PR works either way. Without it, the per-view variable stays, which is still only two lines and keeps everything byte identical. One more thing from the same round of testing, relevant to sizing that follow-up: six of the thirteen inline |
Description
Filament currently requires
script-src 'unsafe-inline', which defeats much of the point of having a Content Security Policy. This adds opt-in nonce support for the scripts Filament renders, as discussed in #7032, and resolves #8329.Registered from a service provider:
Every script rendered by the asset manager, and the inline
window.filamentDatascript, then carry the nonce. The closure is resolved once per request, so all elements in a response share the same value. A closure re-invoked per element would produce a nonce the browser rejects on every tag but the first.Two helpers are available for application and plugin templates:
\Filament\Support\csp_nonce()returns the raw value, ornull\Filament\Support\csp_nonce_html()returns the wholenonce="…"attribute, or an empty stringWhy
FilamentCsprather thanFilamentAssetFollowing up on your question in #7032, CSP turns out not to be purely asset related. Of the inline
<script>tags this work needs to cover, only one is in the asset manager's own view; the rest are in ordinary Blade templates (base.blade.php,sidebar.blade.php,page/index.blade.php, the notifications views). It also cannot go on the panelsFilamentfacade, because assets render without a panel: the scaffolding stub layout uses@filamentScriptsin a plain Laravel layout.Happy to move it if you would prefer it elsewhere.
Scope
This PR covers the nonce API and the asset manager's output only.
Styles are not touched, and are not planned. Per your review on #19230, and because Livewire, Alpine and TipTap all rely on inline styles,
style-src 'unsafe-inline'remains required.Planned follow-ups
Keeping this PR small does mean a panel is not fully CSP compliant the moment it lands, so for context, here is the remaining work in the order I would submit it:
<script>tags in Filament's own Blade templates. 12 tags across 6 files. The dark-mode script inbase.blade.phpis the only violation left on a standard panel page after this PR, and that step removes it.'unsafe-eval'can be dropped too. Around 29 expressions plus 5x-htmlusages, concentrated insupport,tablesandpanels, with a lot of duplication between them.livewire/updateresponses. A nonce is per response, but Livewire morphs later responses into a document served under an earlier one. This may belong in Livewire rather than Filament, so I would establish that before opening anything.Each would be a separate PR, opened when it is complete rather than as a WIP.
Known limitations, documented
Filament cannot add nonces to content it does not render: render hooks, the
scriptsBlade stack,RawJs, and scripts passed toJs::html()as raw<script>markup. The docs explain that authors add the nonce themselves in those cases.The docs also note that the CSP middleware must be registered in the panel's
middleware()rather than Laravel'swebgroup. Panels build their own stack and never resolveweb, so a policy registered onwebsilently never applies to panel routes.Visual changes
None. Until a nonce is configured, the rendered output is unchanged byte for byte, verified by capturing
renderScripts()andrenderStyles()before and after the change and diffing them.Functional changes
composer cscommand.Tested with the full suite (7154 passing, including browser tests) and against a real v5 application with third-party panel plugins, served under
script-src 'self' 'unsafe-eval' blob: 'nonce-…'with no'unsafe-inline'. Every script Filament rendered carried the nonce, and a single distinct nonce appeared throughout the page, matching the header.'unsafe-eval'was kept because Livewire and Alpine still need it untilcsp_safeis enabled, which is separate work.