CSP Nonce plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

CSP Nonce by asign

Community

Per-request CSP nonce, a Content-Security-Policy header and violation reports for Filament panels.

Tags: Developer Tool Panels
Supported versions:
5.x
asign avatar Author: asign

Package health

Automated checks of this plugin's Composer package

100 / 100
Security 100
Maintenance 100
Ecosystem 100
15 checks
  • Passed: GitHub Actions pinned to SHA
  • Skipped: GitLab CI includes pinned to SHA
  • Passed: Open security advisories
  • Passed: Dependabot PR responsiveness — No open Dependabot PRs.
  • Skipped: Renovate MR responsiveness
  • Passed: Dependabot or Renovate configured
  • Passed: Dependency update cooldown configured
  • Passed: Provides a security policy
  • Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
  • Passed: Commit and release recency — Active: last commit 1 days ago; last release 1 days ago.
  • Passed: composer.lock not committed by library — composer.lock is absent from the released dist archive.
  • Passed: Dist archive is lean
  • Passed: Current Laravel version supported — Package dependencies resolve together with current Laravel 13.0.
  • Passed: Current PHP version supported — Constraint ^8.3 supports current PHP 8.5.
  • Skipped: Current Symfony version supported
Third-party plugin. This is built by the community, not the Filament team. Filament does not review, endorse, or vet the security of plugins outside the filament/ namespace. Review the source and install at your own risk. Found malware or an unresolved security issue the author won't address? Report it .
Powered by Plumb Last scanned 12 hours ago

Documentation

Stand With Ukraine Latest Version on Packagist Tests Total Downloads License Plumb score

Filament CSP Nonce

A per-request CSP nonce, a ready Content-Security-Policy header and a violation report endpoint for Filament panels, without overriding a single Filament view.

Without nonces, a Content-Security-Policy for Filament needs 'unsafe-inline' for scripts, which defeats the point (filamentphp/filament#7032, #8329). Filament and Livewire already print the nonce on their asset tags when Laravel's Vite::useCspNonce() is set, but a handful of Filament's own inline <script> and <style> tags have none. This package sets the nonce, closes those gaps and sends the header.

Read What this does and does not protect before relying on it. Filament needs 'unsafe-eval'. That is a property of Alpine, not of this package.

#Screenshots

The Content-Security-Policy header the plugin sends for a panel page (default strict-dynamic preset, captured from the Testbench workbench; the nonce is different on every request). All 18 <script>/<style> tags of the list page carry it.

Response headers with the strict-dynamic policy

#Requirements

  • PHP 8.3+, Laravel 12 or 13, Filament 5 (tested on 5.9, Livewire 4.4).

#Installation

composer require asignua/filament-csp-nonce
php artisan vendor:publish --tag=csp-nonce-config            # optional
php artisan vendor:publish --tag=csp-nonce-migrations         # only for database report storage
php artisan view:clear                                        # once: the Blade rewriter works at compile time

No assets to publish (filament:assets is not needed: the package ships no CSS or JS).

#Usage

use Asignua\FilamentCspNonce\CspNoncePlugin;

$panel->plugin(
    CspNoncePlugin::make()
        ->reportOnly(),            // start here, look at the reports, then remove it
);

Everything is optional:

CspNoncePlugin::make()
    ->preset(Preset::Compatible)                       // or Preset::StrictDynamic (default)
    ->directives([                                     // merged over the preset; null removes a directive
        'connect-src' => ["'self'", 'wss://ws.example.com'],
        'img-src' => ["'self'", 'data:', 'https://cdn.example.com'],
    ])
    ->allowInlineStyles()                              // ColorPicker, CodeEditor (see Gotchas)
    ->policy(fn (CspPolicy $preset) => $preset->directive('frame-src', ["'self'", 'https://www.youtube.com']));

Outside panels (your public pages), use the middleware alias and the helpers:

Route::middleware('csp.nonce')->group(...);   // csp.nonce:admin uses the policy of panel "admin"
<script @cspNonce>...</script>                {{-- nonce="..." --}}
{{ csp_nonce() }}                             {{-- the raw value, or null --}}

#Presets

Preset script-src style-src
filament-strict-dynamic (default) 'nonce-…' 'strict-dynamic' 'unsafe-eval' 'self' 'nonce-…' + style-src-attr 'unsafe-inline'
filament-compatible 'self' 'nonce-…' 'unsafe-eval' (same-origin scripts keep working, e.g. third-party plugin assets) 'self' 'unsafe-inline'

Both add default-src 'self', object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'self', img-src 'self' data: blob: https:, font-src 'self' data:, connect-src 'self', media-src 'self' blob: data:, worker-src 'self' blob:, report-uri and report-to.

->directives() and ->allowInlineStyles() accumulate in any order; a later value for the same directive wins. A CspPolicy instance passed to ->policy() is cloned per request, so it can be shared between panels.

#Violation reports

POST /csp/report (no session, no CSRF, throttled) accepts both report-uri and Reporting API bodies, strips query strings, caps the payload at 16 KB and stores per report.storage: log (default), database (publish the migration; identical violations fold into one row with a hit counter; prune with php artisan csp:prune) or null.

The endpoint is public and unauthenticated, so every report field is attacker-controlled. The limits below protect storage (the table, the log, the cache), not report completeness: a flood of forged reports can use up the per-minute budget or fill the row cap and crowd out genuine violations, so treat a report-only rollout as a hint, not as proof that nothing breaks.

  • reports about a document on another host are dropped (allowed: the request host, the host of app.url, report.allowed_hosts). This filters misrouted reports, it is not a defence: the request host comes from the Host header unless the app trusts only known hosts (TrustHosts), and random paths on the real host pass anyway;
  • at most report.max_new_per_minute (100) new violations are stored or logged per minute across all clients; repeats of a known violation only bump its counter (database) or are logged once per hour (log);
  • the table holds at most report.max_rows (10 000) rows;
  • report.throttle (300,1) limits requests per IP. Firefox POSTs once per violation, so a busy page under report-only sends many, and behind a proxy without TrustProxies all users share one IP: raise it if reports go missing (429). A Reporting API batch is cut at 20 entries.
  • with database storage csp:prune is registered in the scheduler (report.prune_schedule, daily: a parameterless frequency method such as hourly/weekly, or a cron expression such as 15 3 * * *; anything else is logged as a warning and not scheduled; null turns it off). You still need schedule:run in cron.

There is no UI for stored violations: query the table (or build a Filament resource on Asignua\FilamentCspNonce\Models\CspViolation).

#What this does and does not protect

What you get with the default preset (measured in a real browser against Filament 5.9, see FEASIBILITY.md):

  • No inline script runs unless it carries this request's nonce: injected <script> tags, onerror=-style handlers and javascript: URLs are blocked. No host allow-list to bypass (strict-dynamic).
  • object-src, base-uri, form-action and frame-ancestors locked down.
  • Style elements are nonce-only; violation reports tell you what else a page needs.

What you do not get:

  • 'unsafe-eval' stays. Alpine compiles every x-* expression with new Function, and Filament's templates use full JavaScript in them. Livewire 4's CSP-safe build (livewire.csp_safe) was tried: Alpine's CSP parser rejects Filament's expressions and the panel stops working. Consequence: an attacker who can inject HTML into a region Alpine initialises can still run JavaScript via an x-init/x-on:* attribute. Keep escaping output; treat this as defence in depth, not as a replacement for it.
  • style="..." attributes stay allowed (style-src-attr 'unsafe-inline'): Filament renders up to ~20 per page and CSS cannot be nonced on attributes.

#Configuration

config/csp-nonce.php: enabled (env CSP_ENABLED), report_only (CSP_REPORT_ONLY), preset, directives, report.* (enabled, path, throttle, storage, log_channel, table, retention_days, allowed_hosts, max_new_per_minute, max_rows, prune_schedule) and blade.* (rewrite, packages fnmatch patterns of Composer packages whose templates get nonces, paths).

Third-party Filament plugins that print bare <script>/<style> tags: add their package to blade.packages (['filament/*', 'awcodes/*']) and run php artisan view:clear.

#Gotchas

  • php artisan view:clear after installing or changing blade.*. The rewriter runs when a view is compiled; already-compiled views keep their old tags.
  • The rewriter only touches template source, never rendered output, so HTML injected by a user does not receive a nonce. Tags inside @verbatim, @php ... @endphp and <?php ... ?> are skipped (Blade does not compile them). A tag that already prints a nonce is left alone: a nonce / :nonce / x-bind:nonce attribute, @cspNonce, or a Blade echo mentioning a nonce. The word anywhere else (a src path, data-nonce-*, an x-data expression) does not count.
  • Another CSP package next to this one (spatie/laravel-csp, a hand-written middleware): run only one nonce producer. The header follows whatever nonce Vite::useCspNonce() holds when the response leaves this middleware, so a producer inside it is fine; one outside it gets its Vite nonce overwritten, and its own header no longer matches the tags. An existing header of the same name is never replaced.
  • ColorPicker and CodeEditor inject a nonce-less <style> at runtime (CodeMirror, Pickr). They look broken under the strict preset until you call ->allowInlineStyles() (or whitelist the style hashes from your reports). The rich editor works: its Tiptap CSS is printed server-side (->tiptapStyle(false) disables it).
  • Websockets (Echo/Reverb), CDNs, maps, Google Fonts need entries in connect-src / img-src / font-src / style-src; run ->reportOnly() first and read the reports.
  • Nonces are per request. Do not cache full HTML responses (CDN page cache, Cache::remember of rendered views) together with the header.
  • Do not re-add 'unsafe-inline' to script-src next to a nonce: browsers then ignore 'unsafe-inline', but it signals the policy was copied from somewhere that needed it.
  • After a Filament upgrade, TiptapStyleTest fails if Tiptap's bundled CSS changed: copy it into resources/css/tiptap-core.css.

#Uninstalling

Compiled views reference \Asignua\FilamentCspNonce\Nonce. Run php artisan view:clear right after removing the package, or every panel page fails with "class not found".

#Translations

The package has no UI strings, so no language files.

#AI agents

Laravel Boost guidelines ship in resources/boost/guidelines/core.blade.php.

#Testing

composer install
vendor/bin/phpunit
vendor/bin/phpstan analyse
vendor/bin/pint --test

browser/audit.mjs is a manual, real-browser audit (puppeteer-core + Edge/Chrome) against the workbench; it is how the claims above were measured.

#Changelog

See CHANGELOG.

#License

MIT. See LICENSE.

The author

asign avatar Author: asign

asign is a small web-dev company from Lviv, Ukraine. We build business applications on Laravel and Filament — CRMs, automation systems for standard and non-standard business processes, booking and content management systems, including our own Filament-based CMS. We open-source the parts that prove useful beyond a single project

Plugins
15
Stars
6

From the same author