Laravel + Livewire Starter Kit の CSP を nonce 化
Livewire Starter Kit の Docker 検証・改良版フォークで組んだ SetSecurityHeaders middleware は、Content-Security-Policy の script-src / style-src に 'unsafe-inline' を残していた。これは React / Vue 変種と揃えるための暫定措置で、Livewire 変種では 実際には不要 と前回の記事でも予告した通り。本記事ではその予告を回収する作業ログと、最…
検証環境
- 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)
Livewire Starter Kit の Docker 検証・改良版フォークで組んだ SetSecurityHeaders middleware は、Content-Security-Policy の script-src / style-src に 'unsafe-inline' を残していた。これは React / Vue 変種と揃えるための暫定措置で、Livewire 変種では 実際には不要 と前回の記事でも予告した通り。本記事ではその予告を回収する作業ログと、最終的に届いた CSP ポリシーを記録する。
deliverable: csp-nonce ブランチ に 1 commit(+67 行)。テスト 37 → 39 passed (92 → 102 assertions)。
姉妹記事 → Livewire スターターキットを検証して改良した
なぜ Livewire では nonce 化できるのか
訂正 (2026-07-23): 本記事は当初、この節で「React / Vue(Inertia)は初期 props を実行可能な inline script で吐くので nonce 化には Inertia 本体の改造が必要」と書いていた。これは誤りである。Inertia が出力するのは
<script type="application/json">のデータブロックで、ブラウザは実行せずscript-srcの対象外だった。Inertia 側も nonce 化でき、改造は不要である。詳細と実装は Inertia の CSP を nonce 化する記事にまとめた。以下は Livewire 側の説明として維持する。
Livewire は 初期状態を inline script で出力しない:
- クライアント JS(
livewire.js,flux.js)は外部<script src="...">で読み込まれる - 各コンポーネントの状態は HTML 要素の
wire:*属性で保持される - 更新は
/livewire/updateへの fetch リクエストで行われる
つまり Livewire が HTML に吐く要素のうち、'unsafe-inline' を必要とするのは Livewire 自身が注入する小さな起動 script / style だけで、これは Livewire 自体が nonce option を受け取れる作りになっている。
実装を覗くと vendor/livewire/livewire/src/Mechanisms/FrontendAssets/FrontendAssets.php にこういう分岐がある:
protected static function nonce($options = [])
{
$nonce = $options['nonce'] ?? Vite::cspNonce();
return $nonce ? "nonce=\"{$nonce}\"" : '';
}
Vite::cspNonce() を fallback として参照しているので、Vite::useCspNonce($nonce) を middleware で呼ぶだけで Livewire・Vite の両方が同じ nonce を拾う。追加配線は最小で済む。
再確認 (2026-07-23): この実装は vendor 側のものなので、更新で変わると本記事の手順が成立しなくなる。
livewire/livewireの現行mainを確認したところ、上記コードは一字一句そのまま残っていた。
実装
1 ファイル書き換え + 7 テンプレート書き換え + テスト追加で終わる。
SetSecurityHeaders middleware を nonce 生成器に格上げ
handle() の先頭で 16 バイトのランダムバイト列から base64url の nonce を作り、
- コンテナに
csp-nonceとして bind Vite::useCspNonce($nonce)を呼ぶ →@vite(...)と Livewire の自動注入 asset に波及view()->share('cspNonce', $nonce)→ Blade で$cspNonceとして参照可能
$nonce = rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
app()->instance('csp-nonce', $nonce);
Vite::useCspNonce($nonce);
View::share('cspNonce', $nonce);
CSP ポリシーを nonce + strict-dynamic に
'unsafe-inline' を削除、'nonce-{n}' と 'strict-dynamic' を足す:
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' は nonce が通った script が動的に import した子 script を、子 script 側の nonce なしで許可する ディレクティブ。Vite が bundle chunk を動的 import するので必要。
fonts.bunny.net は Starter Kit 既定のフォントホストなので allow-list に追加(Bunny Fonts / Google Fonts いずれも同じパターン)。
Flux ディレクティブに nonce を渡す
@fluxAppearance と @fluxScripts は nonce オプションを受け取れるので、全レイアウトで明示:
@fluxAppearance(['nonce' => $cspNonce ?? null])
@fluxScripts(['nonce' => $cspNonce ?? null])
対象ファイル:
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
既存 inline <style> にも nonce
welcome.blade.php にだけ巨大な inline <style> があるので属性追加:
-<style>
+<style nonce="{{ $cspNonce ?? '' }}">
テスト
tests/Feature/SecurityHeadersTest.php に 2 件追加:
$cspNonceが view に共有され、レスポンス間で値が変わる(nonce の一度性)- 本番環境相当で CSP header を取り、
'unsafe-inline'が含まれない /script-srcとstyle-srcにnonce-...が入る /'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
| 観点 | improvements ブランチ |
csp-nonce ブランチ |
|---|---|---|
script-src |
'self' 'unsafe-inline' |
'self' 'nonce-{req}' 'strict-dynamic' |
style-src |
'self' 'unsafe-inline' |
'self' 'nonce-{req}' https://fonts.bunny.net |
| nonce の経路 | — | middleware → container + Vite + View share |
| Flux directive | @fluxAppearance / @fluxScripts |
@fluxAppearance(['nonce'=>$cspNonce]) / @fluxScripts(['nonce'=>$cspNonce]) |
$cspNonce Blade 変数 |
未定義 | 全ビューから参照可能 |
| テスト | 2 | 4(+ nonce 一度性 / nonce 含有) |
React / Vue 変種への適用可否
適用できる。 本記事は当初ここで「そのままは無理、Inertia 本体の改造が要る」と書いていたが、誤りだった。
Inertia が出力する初期 props は <script type="application/json"> のデータブロックで、ブラウザが実行しないため script-src の対象外である。つまり Inertia 側に nonce を通す必要はない。React / Vue 変種で実際に nonce が要るのは、スターターキット自身が app.blade.php に書いたダークモード判定 <script> と背景色 <style> の 2 箇所だけで、nonce 属性を足すだけで済む。
実際に React 変種で 'unsafe-inline' を撤廃した記録はこちら:
→ Inertia の CSP は nonce 化できる(本体の改造は不要)
よくある質問
Q. Livewire で nonce を配線するには何が必要ですか
middleware で Vite::useCspNonce($nonce) を 1 回呼ぶだけです。Livewire の FrontendAssets::nonce() が $options['nonce'] ?? Vite::cspNonce() というフォールバックを持っているため、Vite と Livewire の両方が同じ nonce を拾います。この実装は現行の livewire/livewire にそのまま残っていることを確認済みです。
Q. Flux のディレクティブにも nonce を渡せますか
渡せます。@fluxAppearance(['nonce' => $cspNonce]) / @fluxScripts(['nonce' => $cspNonce]) の形でオプションを受け取ります。
Q. React / Vue 版にもこの手順は使えますか
使えます。本記事は当初「そのままは無理」と書いていましたが誤りでした。Inertia の出力は実行されないデータブロックなので、Inertia 側に nonce を通す必要はありません。React 変種で実際に撤廃した記録は別記事にあります。
副作用 / 注意点
'strict-dynamic'は 古いブラウザでフォールバック動作が違う(Safari 15.3 以前、古い Chromium 系)。ただし本件のような同一オリジン Vite bundle なら問題は起きにくい。厳密性が要る環境では意図的に外してscript-src 'self' 'nonce-...'だけにしてもよい- CSP 違反を観測したい場合は
report-uri/report-toを付けてContent-Security-Policy-Report-Onlyで dry-run する方法もある。本 commit には入れていない(運用側で追加しやすいようポリシー本体だけに絞った) - Livewire の内部 asset route(
/livewire/livewire.jsなど)は同じオリジンで配信されるのでscript-src 'self'で十分、追加の allow-list は要らない
再現と取り込み
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
improvements から csp-nonce の差分だけ見たい場合:
git diff origin/improvements origin/csp-nonce -- . ':!codelift'
ライセンス / 法務
- 元サンプル: MIT(Laravel LLC / 開発チーム)
- 改良版: MIT(CodeLift / Libra)
検証結果は検証日時点のもの。upstream 更新により状況は変わる可能性がある。
関連記事
- Livewire スターターキットを検証して改良した 公式の laravel/livewire-starter-kit(Livewire v4 + Flux + Alpine)を Docker で動かし、本番運用に耐える形に書き直したフォークを公開する。テストは公式版の 33 passed をベースに 37 passed / 92 assertions(2026-07-23 に Docker で再実行し一致を確認)。本記事は Livewire 固有の論点に絞る — 3 つのスターターキットの中で Livewire は最も ba…
- Inertia の CSP は nonce 化できる(本体の改造は不要) Laravel + Inertia のアプリで Content-Security-Policy から 'unsafe-inline' を外そうとすると、「Inertia が初期 props を inline script に埋めるので無理」「Inertia 本体か HandleInertiaRequests を改造するしかない」という話に行き着くことが多い。
- Laravel で Content-Security-Policy を実装する Content-Security-Policy(CSP)は、XSS(クロスサイトスクリプティング)の被害をブラウザ側で食い止める最後の防御層。サーバーが「このページで実行してよいスクリプト・読み込んでよいリソースはこれだけ」と宣言し、ブラウザがそれ以外を拒否する。