Laravel + Livewire Starter Kit: nonce-based CSP
The SetSecurityHeaders middleware shipped in our Livewire Starter Kit Docker-verified fork kept 'unsafe-inline' in script-src and style-src. That was a deliberate placeholder to match the React/Vue forks; the Livewire architecture doesn't …
Verification environment
- PHP 8.5.5
- Laravel 13.x
- Composer 2.9.7
- Node 22.22.2
- npm 10.9.7
- Frontend Livewire v4 + Flux + Alpine
- Base branch improvements
- Database SQLite (tests)
- OS Docker Desktop (php:8.5-cli-bookworm)
The SetSecurityHeaders middleware shipped in our Livewire Starter Kit Docker-verified fork kept 'unsafe-inline' in script-src and style-src. That was a deliberate placeholder to match the React/Vue forks; the Livewire architecture doesn't actually need it. This article is the follow-up that collects the deferred work: per-request CSP nonce, 'unsafe-inline' removed, CSP locked to 'nonce-{value}' 'strict-dynamic'.
Deliverable: csp-nonce branch, 1 commit (+67 lines). Test suite: 37 → 39 passed (92 → 102 assertions).
Sister article → Hardening the Livewire Starter Kit
Why Livewire can go nonce-only
Correction (2026-07-23): this section originally argued that the React/Vue (Inertia) kits could not go nonce-only without patching Inertia, because Inertia serializes initial props into an executable inline
<script>. That was wrong. Inertia emits a<script type="application/json">data block, which the browser never executes andscript-srctherefore never gates. The Inertia kits can go nonce-based too, with no patching. Details and implementation: Inertia can run a nonce-based CSP. What follows stands as the Livewire-side explanation.
Livewire's architecture:
- Client JS (
livewire.js,flux.js) loads via external<script src="...">. - Component state lives on HTML elements as
wire:*attributes — not as an inline script blob. - Updates go through a fetch to
/livewire/update.
So the only inline things Livewire and Flux emit are small bootstrap <script> and <style> tags, and both packages already accept a nonce option. Digging into vendor/livewire/livewire/src/Mechanisms/FrontendAssets/FrontendAssets.php:
protected static function nonce($options = [])
{
$nonce = $options['nonce'] ?? Vite::cspNonce();
return $nonce ? "nonce=\"{$nonce}\"" : '';
}
It falls back to Vite::cspNonce() — meaning a single Vite::useCspNonce($nonce) call in our middleware makes both @vite(...) output and Livewire's auto-injected asset tags pick up the same nonce. Minimal wiring.
Re-checked 2026-07-23: this code lives in vendor, so an upstream change would quietly invalidate the approach described here. Checked against
livewire/livewireon currentmain— the snippet above is still there verbatim.
Implementation
One middleware rewrite, seven template edits, two test additions.
Promote SetSecurityHeaders to a nonce generator
Generate 16 random bytes as base64url at the start of handle(), then:
- Bind as
'csp-nonce'in the container (for anyone who wants it later). Vite::useCspNonce($nonce)— covers@viteand Livewire asset injection.View::share('cspNonce', $nonce)— exposes it to every Blade template.
$nonce = rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
app()->instance('csp-nonce', $nonce);
Vite::useCspNonce($nonce);
View::share('cspNonce', $nonce);
CSP policy: nonce + strict-dynamic, no 'unsafe-inline'
default-src 'self';
script-src 'self' 'nonce-XYZ' 'strict-dynamic';
style-src 'self' 'nonce-XYZ' https://fonts.bunny.net;
img-src 'self' data: blob:;
font-src 'self' data: https://fonts.bunny.net;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
object-src 'none';
'strict-dynamic' means a nonce-approved script can dynamically import further scripts without those also carrying the nonce — needed for Vite bundle chunks.
fonts.bunny.net is this starter's default font host, so both style-src and font-src allowlist it. Swap for Google Fonts if you're switching.
Pass nonce into Flux directives
Every layout gets ['nonce' => $cspNonce ?? null] added:
@fluxAppearance(['nonce' => $cspNonce ?? null])
@fluxScripts(['nonce' => $cspNonce ?? null])
Files touched:
resources/views/partials/head.blade.phpresources/views/layouts/app/header.blade.phpresources/views/layouts/app/sidebar.blade.phpresources/views/layouts/auth/card.blade.phpresources/views/layouts/auth/simple.blade.phpresources/views/layouts/auth/split.blade.php
Annotate remaining inline styles
welcome.blade.php carries a large inline <style> block (the default Laravel welcome page). One attribute addition:
-<style>
+<style nonce="{{ $cspNonce ?? '' }}">
Tests
Two new assertions in tests/Feature/SecurityHeadersTest.php:
$cspNonceis shared to views and the value changes between responses (single-use property).- The production-mode CSP header excludes
'unsafe-inline', includes a nonce in bothscript-srcandstyle-src, and carries'strict-dynamic'.
public function test_production_csp_includes_nonce_and_forbids_unsafe_inline(): void
{
config()->set('app.env', 'production');
$this->app->detectEnvironment(fn () => 'production');
$response = $this->get('/login');
$csp = $response->headers->get('Content-Security-Policy');
$this->assertStringNotContainsString("'unsafe-inline'", $csp);
$this->assertMatchesRegularExpression("/script-src [^;]*'nonce-[A-Za-z0-9_-]+'/", $csp);
$this->assertMatchesRegularExpression("/style-src [^;]*'nonce-[A-Za-z0-9_-]+'/", $csp);
$this->assertStringContainsString("'strict-dynamic'", $csp);
}
Before / after
| Dimension | improvements branch |
csp-nonce branch |
|---|---|---|
script-src |
'self' 'unsafe-inline' |
'self' 'nonce-{req}' 'strict-dynamic' |
style-src |
'self' 'unsafe-inline' |
'self' 'nonce-{req}' https://fonts.bunny.net |
| Nonce plumbing | — | middleware → container + Vite + View share |
| Flux directives | @fluxAppearance / @fluxScripts |
both passed ['nonce' => $cspNonce] |
$cspNonce in Blade |
not defined | available site-wide |
| Tests | 2 | 4 (adds nonce uniqueness + nonce presence) |
What about the React / Vue forks?
They port over fine. This section originally said they didn't — that patching Inertia was required. That was wrong.
Inertia's initial props ship as a <script type="application/json"> data block, which the browser never executes, so script-src doesn't gate it and there's no nonce to thread through Inertia. The only things needing a nonce in the React / Vue kits are the starter kit's own dark-mode <script> and background-colour <style> in app.blade.php — a nonce attribute on each.
The React variant with 'unsafe-inline' actually removed:
→ Inertia can run a nonce-based CSP (no patching required)
FAQ
Q. What does it take to wire a nonce through Livewire?
One call to Vite::useCspNonce($nonce) in middleware. Livewire's FrontendAssets::nonce() falls back to $options['nonce'] ?? Vite::cspNonce(), so Vite and Livewire both pick up the same value. That fallback is still present in current livewire/livewire — checked.
Q. Do the Flux directives accept a nonce?
Yes: @fluxAppearance(['nonce' => $cspNonce]) and @fluxScripts(['nonce' => $cspNonce]).
Q. Does this transfer to the React and Vue kits?
It does. This article originally said it could not, and that was wrong. Inertia's output is a non-executed data block, so nothing needs threading through Inertia. The React kit's removal is documented separately.
Side effects and notes
'strict-dynamic'has fallback quirks in older browsers (Safari ≤ 15.3, legacy Chromium). Same-origin Vite bundles are unaffected in practice, but if you want to be conservative, drop'strict-dynamic'and live with each script carrying the nonce.- A CSP rollout normally goes through a
Content-Security-Policy-Report-Onlyphase withreport-uri/report-toendpoints to collect violations before enforcing. This commit ships the enforcing policy directly because the test + manual verification covered the known inline surfaces; add a reporter if your traffic mix differs (e.g. custom scripts added downstream). - Livewire's own asset route (
/livewire/livewire.jsetc.) is same-origin, soscript-src 'self'covers it. No extra host allow-listing needed.
Reproduce and adopt
git clone https://github.com/codelift-dev/livewire-starter-kit.git
cd livewire-starter-kit
git checkout csp-nonce
docker compose -f codelift/docker-compose.yml build
docker compose -f codelift/docker-compose.yml run --rm app
Diff against improvements only (excluding the CodeLift verification files):
git diff origin/improvements origin/csp-nonce -- . ':!codelift'
License
- Upstream: MIT (Laravel LLC and contributors)
- Improvement: MIT (CodeLift / Libra)
Findings reflect the state on the verification date; upstream may change.
Related articles
- Hardening the Livewire Starter Kit We took the official laravel/livewire-starter-kit (Livewire v4 + Flux + Alpine), ran it inside Docker, and published a production-hardened fork. Tests go from the upstream 33 passed to 37 passed / 92 assertions — re-run in Docker on 2026-0…
- Inertia Needs No Patch for a Nonce-Based CSP Try to drop 'unsafe-inline' from the Content-Security-Policy of a Laravel + Inertia app and you'll usually land on some version of: "Inertia embeds the initial props in an inline script, so you can't — you'd have to patch Inertia or Handle…
- Implementing Content-Security-Policy in Laravel Content-Security-Policy (CSP) is the last defense layer that stops XSS damage in the browser. The server declares "these are the only scripts allowed to run and resources allowed to load on this page," and the browser rejects everything el…