laravel-cors v2 の HandleCors ミドルウェアは CSRF 対策用途には使えない


はじめに

Web サービスの開発で Laravel × Vue.js という構成を聞くことが多くなってきたのではないでしょうか?
この記事は、私が個人サービスを開発している際にハマった、あるライブラリについてのお話です。

この記事を書くにあたり、私の師匠(自称)であるまっぴー(@mpyw)さんに助言を頂きました。
ありがとうございます!!!!!!

また、ここでは CSRF 対策や同一オリジンポリシーの基礎知識について詳しく掘り下げません。
私の師匠がとても良い記事を書かれているので、そちらを読んでいただき CSRF 対策と同一オリジンポリシーを完全に理解 した上でこの記事を読むとわかりやすいと思います。

これで完璧!今さら振り返る CSRF 対策と同一オリジンポリシーの基礎

laravel-cors って?

ご紹介しましょう、これこそが私がドハマりしたライブラリです。
Laravel において CORS(Cross-Origin Resource Sharing) の導入を行う際に非常に便利なもので、許可するオリジンの設定に正規表現を用いることができたり、様々な拡張に対応しています。
Laravel7 からデフォルトでインストールされるようになっています。

ちょっと整理してみよう

Web サービスを Laravel × Vue.js という構成で開発する場合、大抵以下のふたつの方法があります。

  • Laravel 自体に Vue.js のコンポネートを流し込む
  • 別々のアプリケーションとして開発を行う

Laravel 自体に Vue.js のコンポネートを流し込む場合は、Laravel に標準で用意されているresource/js/bootstrap.js を用いることでLaravel 単体で開発を行うときと同じように、CSRF トークンを使って CSRF 対策を行うことができます。

別々のアプリケーションとして開発を行う場合も、フロントエンドがバックエンドから送られてくる XSRF-TOKEN を用いて CSRF 対策を行うことは可能ですが、XSRF-TOKEN は Cookie でサーバ側から配信されるためステートフルなWebサービスの場合にしか使えません。

また、別々のアプリケーションとして開発を行う場合では

  • example.com (フロントエンド)
  • api.example.com (バックエンド)

のようにドメインを分けて運用されることが多々あります。
このような構成で、フロントエンドがバックエンドに対してリソースを要求すると、セキュリティ上の理由によりブラウザ自身が 同一オリジンポリシー に従ってリクエストの結果の読み取りをブロックします。
つまり、リクエストは送れてもレスポンスは読み取れないよってことです。

同一オリジンポリシーに対して、このドメインに対するリソースの要求は安全なものであることを示し、レスポンスを読み取れるようにするのが CORS (Cross-Origin Resource Sharing) にある Access-Control-Allow-Origin などです。

では、ステートレスなアプリケーションの場合やドメインがフロントエンドとバックエンドで別れている場合はどのような CSRF 対策を施したらよいのでしょうか?

Origin をサーバーサイドで検証しよう

師匠の記事をよく読み、理解されている方はお察しだと思いますが、上記のような場合はサーバーサイド側で Origin ヘッダを検証することで CSRF 対策を行うことができます。

これは、現在閲覧しているサイトのURLを示す Origin ヘッダが特殊な方法を用いなければ偽装することができず、意図しているオリジンからのリクエストであることをサーバーサイド側で検証することができるからです。

本題

私がハマったのは、ズバリ laravel-cors v1.x では Origin ヘッダをしっかりとライブラリで検証していたが、laravel-cors v2.x では Origin ヘッダを検証するのをやめていた という点です。

以下の画像を御覧ください
これは、laravel-cors のベースとなっているライブラリ stack-cors を Origin ヘッダの検証を削除したもの(v2.x)へバージョンアップするためのPRです。


https://github.com/fruitcake/laravel-cors/pull/443

ここには、「厳密なオリジンの検証はブラウザ側に任せ、キャッシュ性を向上させる」とあります。

🔥 CORS ≠ CSRF 対策 🔥

よく、CORS を導入しているから CSRF 対策ができているという話を聞きますが、これは間違いです。
CORS はあくまで、サーバーに対して別ドメインからリソースを要求し、その結果の読み取りをブロックするものであり、サーバーへのリクエストを制限するものではないからです。

よく考えればそれはそう

上記で説明したように、CORS はあくまで返ってきた結果が不正なコードに参照されることを防ぐためのもので、CSRF 対策にはなんら関係がありません
ライブラリとして持つ責任の範囲を明確にして、厳密なオリジンの検証が必要であればユーザーの実装に任せるというもので、とてもよい変更だったと思います。

解決策

laravel-cors v2.x で CSRF 対策ができないことはわかりました。
では、自分でやるしかないでしょう!!!!!
CSRF 対策を自分でするにあたって、要件を書き出してみましょう。

  • メインリクエストでオリジンを検証すること
  • リクエストに Origin ヘッダが含まれていない場合は通す

リクエストに Origin ヘッダが含まれていない場合に通すのは、同一オリジンの場合 Origin ヘッダーを付与しない という挙動をするブラウザがあるためです。

これを踏まえて、ミドルウェアを書いてみましょう!
ここでは師匠が書いてくださっていたコードを引用させていただきます。

VerifyOrigin.php
<?php

namespace App\Http\Middleware;

use Asm89\Stack\CorsService;
use Closure;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

class VerifyOrigin
{
    protected CorsService $cors;

    public function __construct(CorsService $cors)
    {
        $this->cors = $cors;
    }

    public function handle($request, Closure $next)
    {
        // Origin ヘッダがリクエストに含まれていて、かつそのオリジンが許可対象である
        if ($request->hasHeader('Origin') && !$this->cors->isOriginAllowed($request)) {
            throw new AccessDeniedHttpException('The request from your origin is not allowed.');
        }

        return $next($request);
    }
}

このミドルウェアを使うことで、CSRF 対策もしっかりと行うことができます!

おわりに

おわりに、といってもあんまり書くことないんですが…w
今回の記事は、自分の復習のために書いてみました。
CORS や CSRF 対策に関して再確認ができてよかったです!