Laravel はどのようにCSRF対策行っているのか?

2020年3月1日ITLaravel, PHP

Laravel と Blade で開発していたらよく見かける「@csrf」ですが、この「@csrf」が何のために何を行っているのか、仕組みを確認しました。

<form method="POST" action="">
    @csrf
    <!-- 省略 -->
</form>

検証環境

PHP 7.4.2
Laravel 6.15.1

そもそも CSRF とは?

Cross Site Request Forgeries の略です。

Cross : 横切る、交差する
Site : サイト
Request : 要求
Forgeries : 偽造

「サイトへのリクエストを偽造する」と捉えていいかと思います。

Wikipediaの説明がわかりやすいと思うのですが、

https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA

ALICEは攻撃者のMALIAに送金するつもりはないのに、銀行にログインしている状態で、MARIAが用意した不正なURLをクリックしてしまうと、MALIAに送金してしまいます。

ここでALICEは自分が意図してないリクエスト(MALIAに偽造されたリクエスト)を銀行に送ってしまっています。

これを防ぐためにはALICEが意図したリクエスト(偽造されていない正規のリクエスト)であることを銀行側が確認する必要があります。

Laravel はどのようにCSRF対策を行っているのか?

ALICEが意図したリクエストであることを銀行側が確認するために、最初に登場した「@csrf」を使用します。

「@csrf」が埋め込まれた箇所をブラウザで確認すると下記のようにトークンが埋め込まれています。

<form method="POST" action="">
    <input type="hidden" name="_token" value="atgSAXPi0DtlJt3XaRtxmZegFOjR1rNOKprPA5mG"> 
    <!-- 省略 -->
</form>

このブラウザに埋め込まれたトークンを使って、ALICEが意図したリクエストであることをは確認します。

参考:

https://laravel.com/docs/6.x/csrf

Laravel はどのようにトークンを確認しているのか?

リクエストの「_token」とセッションに含まれている「token」が等しいかで確認しています。

実際に処理している箇所は下記の tokensMatch メソッドです。

Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php

public function handle($request, Closure $next)
{
    if (
        $this->isReading($request) ||
        $this->runningUnitTests() ||
        $this->inExceptArray($request) ||
        $this->tokensMatch($request) // <-- ここ
    ) {
        return tap($next($request), function ($response) use ($request) {
            if ($this->shouldAddXsrfTokenCookie()) {
                $this->addCookieToResponse($request, $response);
            }
        });
    }

    throw new TokenMismatchException('CSRF token mismatch.');
}

下記の getTokenFromRequest メソッドでブラウザから送られてきた _token を取得します。

セッションにある token とブラウザから送られてきた _token と2つの文字列それぞれのトークンが文字列かチェックして、hash_equals で「2つの文字列が等しいかどうか、同じ長さの時間で」確認します。

/**
* Determine if the session and input CSRF tokens match.
*
* @param  \Illuminate\Http\Request  $request
* @return bool
*/
protected function tokensMatch($request)
{
    $token = $this->getTokenFromRequest($request);

    return is_string($request->session()->token()) &&
            is_string($token) &&
            hash_equals($request->session()->token(), $token); <-- ここ
}

/**
* Get the CSRF token from the request.
*
* @param  \Illuminate\Http\Request  $request
* @return string
*/
protected function getTokenFromRequest($request)
{
    $token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');

    if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
        $token = $this->encrypter->decrypt($header, static::serialized());
    }

    return $token;
}

参考:

@csrfを含めないでPOSTするとどうなるか?

最初に登場した下記のコードで@csrfを書かないで、POSTしますと、、、

<form method="POST" action="">
    <!-- 省略 -->
</form>
419|Page Expired

419 エラーになります。

これを除外するには「@csrf」を書いてトークンを埋め込むか、リクエストのミドルウェアからVerifyCsrfTokenを除く必要があります。

Laravelのデフォルトの設定では、ミドルウェアグループに設定されています。

app/Http/Kernel.php

/**
* The application's route middleware groups.
*
* @var array
*/
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class, <-- ここ
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
]

先述のALICEが行った送金処理などの「重要な処理」の場合は銀行側はリクエストがALICEからのリクエストであることを確認する必要がありますが、

「重要な処理」ではないリクエストの場合はCSRF対策は必要ないので、設計時にCSRF対策が必要なページと必要でないページを区別しておくのがいいかと思います。

以上になります。

ここまでお読みいただきありがとうございました。

スポンサーリンク

Posted by nobuhiro harada