Add opt-in CSP nonce support for scripts(null)The user wants a changelog article for filamentphp/filament about PR #20301. I must follow the system prompt exactly: Markdown only, no leading H1 repeating the title, no horizontal rules, 600-900 words, 4-6 s
Contributed by m-develops
Part of the filament v5.10.1 release
Category: 2 — Feature
Until now, running a Filament panel behind a strict Content Security Policy was practically impossible: Filament renders several inline <script> tags, so any policy had to allow script-src 'unsafe-inline', which defeats much of the point of having a CSP at all. This release adds opt-in nonce support for the scripts Filament renders (#20301). When a nonce is configured for the request, every script tag the asset manager produces — registered scripts, registered script data, and the inline window.filamentData bootstrap — carries a matching nonce attribute. If you don't opt in, the rendered output is byte-for-byte identical to v4.15.1, verified by diffing renderScripts() and renderStyles() before and after the change, so upgrading carries no behavioural risk.
What changed
Filament now reads the request's CSP nonce from Laravel's Vite facade — set with Vite::useCspNonce(), read with Vite::cspNonce() — and appends it to the script tags it renders. The choice of Vite is deliberate: Livewire reads the same value, so one shared nonce keeps Laravel, Livewire and Filament in agreement, and the browser sees a single nonce across the page that matches the Content-Security-Policy header. Note that the facade is only used as a shared store: your application does not need to compile assets with Vite or use the @vite directive, so this works equally well with Laravel Mix or any other tooling.
How to enable it
Generate the nonce in middleware, share it with Vite, and send the header. With spatie/laravel-csp, its request-scoped nonce is resolved as app('csp-nonce'):
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class SetCspNonce
{
public function handle(Request $request, Closure $next): Response
{
Vite::useCspNonce(app('csp-nonce'));
return $next($request);
}
}
Then register it on the panel, not on the web group:
use App\Http\Middleware\SetCspNonce;
use Filament\Panel;
use Spatie\Csp\AddCspHeaders;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->middleware([
// ...
SetCspNonce::class,
AddCspHeaders::class,
]);
}
That placement matters more than it looks: panels build their own middleware stack and never resolve Laravel's web group, so a policy appended to web silently never applies to panel routes — a common cause of a policy that appears configured but does nothing. Registering on the panel also scopes the policy to that panel, leaving the rest of your application untouched. Finally, call Vite::useCspNonce() for every request, before the response renders — not once in a service provider's boot(), since the nonce must rotate between requests and service providers are not re-booted under long-running workers such as Octane.
Your own scripts
Filament can only add nonces to markup it renders. Render hooks, the scripts Blade stack, RawJs, and Js::make()->html() calls whose HTML already contains a <script> element are output exactly as you provide them, so add the nonce yourself:
<script nonce="{{ \Illuminate\Support\Facades\Vite::cspNonce() }}">
// ...
</script>
What this doesn't cover yet
Be clear-eyed about scope: configuring the nonce does not, by itself, let you remove 'unsafe-inline' from script-src on a standard panel. Three pieces of follow-up work remain, each planned as a separate PR:
- Filament's own Blade templates still contain inline scripts without nonces — the dark-mode script in
base.blade.phpis the last violation left on a standard panel page after this change. - Alpine expressions the CSP-safe evaluator cannot parse mean
'unsafe-eval'is still required until separatecsp_safework lands. - Nonce behaviour across
livewire/updateresponses is still being established — a nonce is per response, but Livewire morphs later responses into a document served under an earlier one — and may belong in Livewire itself.
Styles are untouched and not planned: Livewire, Alpine and TipTap all rely on inline styles, so style-src 'unsafe-inline' remains required.
Upgrade impact
For most users, none: the feature is opt-in, and existing applications upgrade without action. If you enable it, the attribute is HTML-escaped — test coverage confirms a misconfigured nonce cannot inject markup. The change was validated 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, a single distinct nonce appeared throughout the page, and it matched the header. The full suite passes with 7,154 tests, including browser tests.
Takeaways
- Scripts rendered through the asset manager — including the inline
window.filamentDatascript — now carry anonceattribute when one is configured; without configuration, output is unchanged. - Configure it with
Vite::useCspNonce($nonce)per request; the same nonce is shared with Livewire, and no Vite build step is needed. - Register CSP middleware in the panel's
middleware(), not thewebgroup, or the header never reaches panel routes. - Add the nonce yourself to render hooks, the
scriptsstack,RawJs, andJs::make()->html()markup usingVite::cspNonce(). - You cannot drop
'unsafe-inline'fromscript-srcyet — Filament's own inline scripts and'unsafe-eval'requirements are addressed in follow-up PRs, andstyle-src 'unsafe-inline'remains necessary. }