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トークンみたいなもん。
以下のような処理で認証が実行される
- サーバー側
- リクエストごとにランダムな
nonce
を生成 - CSPヘッダーに
nonce
を設定 - HTMLの
<script>
や<style>
タグにも同じnonce
を追加
- リクエストごとにランダムな
- ブラウザ側
- CSPヘッダーとHTMLタグの
nonce
を比較 - 一致すれば実行許可、不一致なら拒否
- CSPヘッダーとHTMLタグの
また、なぜドメインのホワイトリスト形式でなく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()
{
...
}
}
以上です。