laravel-cspでLaravelでCSPを設定する

投稿日 最終更新日

2024年12月17日

LaravelでCSP(Content-Security-Policy)を設定しようと思ったので、その備忘録。

CSP(Content-Security-Policy)とはなにか

詳しくはMDNのコンテンツセキュリティポリシーを参照。

現時点での私の理解は

  • JS、CSS、画像、フォントなどの読み込み元や実行ルールを指定することで、XSSなどを防ぎセキュアにすることを目的としている
  • HTTPヘッダーまたは<meta>で指定可能
  • W3Cによって定義されおり、現在では新しいものでLevel3が草案として存在している
  • ブラウザの互換性を見る限り、主要なブラウザであればLevel3の仕様に対応している。逆に言えばブラウザによっては対応していなかったりするものもあるのでセーフティネット程度に使うのが良さそう

spatie/laravel-cspを使ってみる

なんと、LaravelでCSPを設定するためのspatie/laravel-cspというライブラリがあるのでこれを使ってみる。

以下でインストール

composer require spatie/laravel-csp

以下で設定を公開

php artisan vendor:publish --tag=csp-config

初期の中身は以下のような感じ。
コメントは機械和訳した。

<?php

return [

    /*
     * どの CSP ヘッダーが設定されるかはポリシーによって決まります。
     * 有効な CSP ポリシーは、「Spatie\Csp\Policies\Policy」 を拡張するクラスです。
     */
    'policy' => Spatie\Csp\Policies\Basic::class,

    /*
     * このポリシーはレポート専用モードになります。これはテストに最適です
     * 新しいポリシー、または何も壊すことなく既存の CSP ポリシーを変更します。
     */
    'report_only_policy' => '',

    /*
     * ポリシーに対するすべての違反は、この URL に報告されます。
     * これに使用できる優れたサービスは https:report-uri.com です。
     *
     * ポリシーで「reportTo」を呼び出すことで、この設定をオーバーライドできます。
     */
    'report_uri' => env('CSP_REPORT_URI', ''),

    /*
     * ヘッダーは、この設定が true に設定されている場合にのみ追加されます。
     */
    'enabled' => env('CSP_ENABLED', true),

    /*
     * インラインタグとヘッダーで使用されるnoncesの生成を担当するクラス。
     */
    'nonce_generator' => Spatie\Csp\Nonce\RandomString::class,
];

とりあえず設定はそのままでapp/Kernel.phpにミドルウェア登録をしてみる

protected $middlewareGroups = [
   'web' => [
       ...
       \Spatie\Csp\AddCspHeaders::class,
   ],

この状態でサイトにアクセスするとHTTPヘッダーに

content-security-policy:base-uri 'self';connect-src 'self';default-src 'self';form-action 'self';img-src 'self';media-src 'self';object-src 'none';script-src 'self' 'nonce-Kh3YeOsH2KaiGZvoVF1yDmxN2hmUNL3W';style-src 'self' 'nonce-Kh3YeOsH2KaiGZvoVF1yDmxN2hmUNL3W'

のようにデフォのCSPが設定される。

これは、configの'policy' => Spatie\Csp\Policies\Basic::class,でBasicPolicyが設定されているから。

CSPをカスタマイズする

laravel-cspのPolicyクラスを自作して独自のルールを適用する。

公式ドキュメントではapp/SupportでPolicyを作成しているので、同じように作る。

つまり、app/Support/CustomCspPolicy.phpを作りお試しで以下のようにしてみる。

<?php

namespace App\Support;

use Spatie\Csp\Directive;
use Spatie\Csp\Keyword;
use Spatie\Csp\Policies\Policy;

class CustomCspPolicy extends Policy
{

    public function configure()
    {
        $this
            ->addDirective(Directive::SCRIPT, Keyword::SELF);
    }
}

そして、configでこのクラスを指定し、ヘッダーをみてみると

content-security-policy:script-src 'self'

のように好きにCSPが付けられるようになった。

Viteとlaravel-cspをnonceで連携させる

Viteで生成したCSSやJSをnonceという仕組みで連携させる。

nonceっていうのはブラウザが使うCSRFトークンみたいなもん。
以下のような処理で認証が実行される

  • サーバー側
    1. リクエストごとにランダムなnonceを生成
    2. CSPヘッダーにnonceを設定
    3. HTMLの<script><style>タグにも同じnonceを追加
  • ブラウザ側
    1. CSPヘッダーとHTMLタグのnonceを比較
    2. 一致すれば実行許可、不一致なら拒否

また、なぜドメインのホワイトリスト形式でなくnonceを使うのかはContent Security Policy Level 3におけるXSS対策などを参照してもらえばと思う。

まず、Viteが生成する<script>などにnonceを自動付与してもらうために、カスタムミドルウェアを作りuseCspNonceメソッドを利用する。

以下でミドルウェアを作成し

php artisan make:middleware AddViteUseContentSecurityPolicy

中身は以下のようにした

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;

class AddViteUseContentSecurityPolicy
{
    public function handle(Request $request, Closure $next): Response
    {
        Vite::useCspNonce();

        return $next($request);
    }
}

これでViteが自動的にnonceを付与してくれるようになったので、laravel-cspにそのnonceを使うよう指示する

app/Support/LaravelViteNonceGenerator.phpを作成し以下のようにする。

<?php

namespace App\Support;

use Illuminate\Support\Facades\Vite;
use Spatie\Csp\Nonce\NonceGenerator;

class LaravelViteNonceGenerator implements NonceGenerator
{
    public function generate(): string
    {
        return Vite::cspNonce();
    }
}

そしてこのクラスをlaravel-cspのconfigのnonce_generatorに適用することで、Viteが生成したScriptやStyleにnonceが自動で付き、CSPヘッダーにも同じnonceの値が設定されていることがわかると思う。

インラインスクリプトをnonceで許可する

「Viteとlaravel-cspをnonceで連携させる」で行った通り、カスタムミドルウェアを作りuseCspNonceメソッドを適用までは終わっている前提で。

ここまでしていれば後は

<script nonce="{{ csp_nonce() }}">

のようにタグ内にnonceを指定することでおk。

Telescopeを許可する

CSPでnonceの設定をしているとTelescopeが全然見えない。

私は一応以下のように回避

    public function configure()
    {
        if (config('app.debug') && Request::is("telescope*")) {
            return;
        }
        ...
    }

本番環境でもTelescopeを使う予定の人は他の方法を推奨。

エラー表示(Ignition)を許可する

Telescopeに続き、デバッグのエラー画面を生成するIgnitionもCSPを設定していると表示できない。

解決策はGitHubのIssueからの受け売りなんだけど、PolicyクラスのshouldBeAppliedメソッドをオーバーライドする形で回避

class CustomCspPolicy extends Policy
{
    public function shouldBeApplied(\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response): bool
    {
        if (config('app.debug') && (
                $response->isClientError() || // Ignition
                $response->isServerError() // Ignition
            )) {
            return false;
        }

        return parent::shouldBeApplied($request, $response);
    }

    public function configure()
    {
        ...
    }
}

以上です。