{} CodeLift

Laravel + Livewire Starter Kit の CSP を nonce 化

Livewire Starter Kit の Docker 検証・改良版フォークで組んだ SetSecurityHeaders middleware は、Content-Security-Policy の script-src / style-src に 'unsafe-inline' を残していた。これは React / Vue 変種と揃えるための暫定措置で、Livewire 変種では 実際には不要 と前回の記事でも予告した通り。本記事ではその予告を回収する作業ログと、最…

公開 2026-04-19 検証 2026-04-19 更新 2026-04-19

検証環境

  • 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)

Laravel + Livewire Starter Kit の CSP を nonce 化

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)

姉妹記事 → Laravel + Livewire Starter Kit を Docker で検証して改良した fork を公開

なぜ Livewire では nonce 化できるのか

React / Vue の Inertia 実装は、初期 props を HTML に埋め込むときに次のような inline script を吐く:

<div id="app" data-page="{...encoded props...}"></div>
<!-- かつて: -->
<script>window.__INERTIA__ = {...};</script>

この inline script を許容するために 'unsafe-inline' を CSP に残すか、nonce を各 script タグに埋め込む必要がある。nonce 方式にするには HandleInertiaRequests を改造して nonce を通す実装が必要で、CodeLift の React / Vue 記事では「その改修はスコープ外」として保留した。

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 を拾う。追加配線は最小で済む。

実装

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.php
  • resources/views/layouts/app/header.blade.php
  • resources/views/layouts/app/sidebar.blade.php
  • resources/views/layouts/auth/card.blade.php
  • resources/views/layouts/auth/simple.blade.php
  • resources/views/layouts/auth/split.blade.php

既存 inline <style> にも nonce

welcome.blade.php にだけ巨大な inline <style> があるので属性追加:

-<style>
+<style nonce="{{ $cspNonce ?? '' }}">

テスト

tests/Feature/SecurityHeadersTest.php に 2 件追加:

  1. $cspNonce が view に共有され、レスポンス間で値が変わる(nonce の一度性)
  2. 本番環境相当で CSP header を取り、'unsafe-inline' が含まれない / script-srcstyle-srcnonce-... が入る / '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 変種への適用可否

そのままは無理。React / Vue 変種では Inertia が初期 props を inline <script> で吐くので、nonce 化するには:

  1. middleware で nonce を生成するのは同じ
  2. 追加で HandleInertiaRequests を改造して、Inertia がレンダリングする inline script タグに nonce を埋める
  3. あるいは Inertia の server-side rendering(SSR)に切り替えて inline script を外部化

(2) は Inertia 本体へのパッチまたは decorator 実装が必要で、scope としては「CodeLift の別記事」レベル。興味があれば follow-up を書く。

副作用 / 注意点

  • '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 improvements csp-nonce -- . ':!codelift'

ライセンス / 法務

  • 元サンプル: MIT(Laravel LLC / 開発チーム)
  • 改良版: MIT(CodeLift / JIT 株式会社)

検証結果は検証日時点のもの。upstream 更新により状況は変わる可能性がある。

関連記事