コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

LaravelのCVE修正diffで学ぶWebセキュリティ — 第3回:認証フローと整合性の前提を崩す攻撃(A07・A08)

はじめに

こんにちは。プロダクト開発部の塚原です。

第1回では「設定の油断が招く脆弱性(A02)」、第2回では「入力値と暗号の落とし穴(A04・A05)」として、タイミング攻撃やバインディング操作といった事例を紹介してきました。

第3回では、A07(Authentication Failures)とA08(Software or Data Integrity Failures)を取り上げます。いずれも「当たり前に信頼していた前提」が崩されるタイプの脆弱性です。処理時間の差、HTTPヘッダの値、復号したデータの中身など。普段あまり気に留めない要素が、攻撃の入口になってしまいます。3件のCVEを通じて、その「前提の疑い方」を見ていきましょう。

今回取り上げる事例

# CVE/識別子 カテゴリ
事例1 CVE-2022-40482 A07
事例2 CVE-2017-9303 A07
事例3 CVE-2018-15133 A08

⚠️本記事は学習目的で過去の修正済み脆弱性を解説するものです。具体的な攻撃の再現手順は記載していません。すべての脆弱性は修正済みであり、最新バージョンへのアップデートを推奨します。


A07: Authentication Failures(認証の失敗)

2021年版から順位を変えず7位にとどまっているカテゴリです。クレデンシャルスタッフィング、ブルートフォース、デフォルトパスワード、セッション管理の不備、MFAの欠如など、認証の仕組み全般の脆弱性を含みます。

認証の「前提」はどこに潜むか

ログインフォームのような一見シンプルな機能であっても、実装には多くの暗黙の前提が含まれています。「ユーザーの存在有無でレスポンス時間に差が出てはいけない」「Hostヘッダは信頼できない」など。こうした暗黙の前提に注意できるかどうかが、認証機能の堅牢さを左右します。これから紹介する2件は、どちらも「前提の見落とし」から生まれた脆弱性です。


事例1: タイムレスタイミング攻撃によるユーザー列挙(CVE-2022-40482)

影響バージョン 8.x 〜 9.x(< 9.32.0)
CVSS 6.5(Medium)
参照 GHSA-5qxg-5vwh-7j5jPR #44069修正commit解説記事

SessionGuard::hasValidCredentials() でユーザーが存在しない場合に早期リターンしていたため、HTTP/2 の多重化を使ったタイムレスタイミング攻撃でユーザーの存在有無を判別できる脆弱性です。以下の修正前のコードを見たとき、なぜレスポンス時間に差が出てしまうのか少し考えてみましょう。

<?php
// 修正前のコード: ユーザーが存在しない場合、パスワード検証をスキップして早期リターン
protected function hasValidCredentials($user, $credentials)
{
    $validated = ! is_null($user) &&
        $this->provider->validateCredentials($user, $credentials);

    if ($validated) {
        $this->fireValidatedEvent($user);
    }

    return $validated;
}
問題点:ハッシュ検証のスキップがタイミング差として露出する

ユーザーが存在しない場合はハッシュ比較(validateCredentials())が実行されないため、レスポンスがわずかに速くなります。従来のタイミング攻撃ではネットワークジッターに埋もれていた差ですが、HTTP/2 の多重化を使うとリクエストを同一パケットにまとめて送信でき、ネットワーク遅延の影響を排除できます(Timeless Timing Attacks, USENIX Security 2020)。解説記事によると、サンプルサイズわずか 6組のリクエストペア(=6回の比較測定)で 20 マイクロ秒の差を検出可能とされています。

想定される被害: 「このメールアドレスは登録済みか」を攻撃者が判別でき、フィッシングやブルートフォース攻撃の事前調査に使われる。

CVSS 6.5 の理由: 情報漏洩に限られるものの、ネットワーク越しに特別な権限なしで実行できる点が評価されています。

修正diff:Timeboxによる実行時間の均一化
<?php
// 修正後: Timeboxで一定時間かかるように保証
protected function hasValidCredentials($user, $credentials)
 {
+    return $this->timebox->call(function ($timebox) use ($user, $credentials) {
        $validated = ! is_null($user) &&
            $this->provider->validateCredentials($user, $credentials);

        if ($validated) {
+            $timebox->returnEarly();
            $this->fireValidatedEvent($user);
        }

        return $validated;
+    }, 200 * 1000); // 200ミリ秒
 }

新たに Laravelに導入された Timebox クラスにより、ユーザーが存在しなくても最低 200 ミリ秒はかかるようにしてタイミング差を排除しています。認証成功時は returnEarly() で即座にレスポンスを返すため、正規ユーザーの体験は損ないません。認証成功はレスポンス内容自体から判別できるため、成功時のレスポンス速度がタイミング情報として新たに漏れることはありません。

第2回の hash_equals() が「データの比較」における定数時間を保証するのに対し、この Timebox は「処理全体の実行時間」における定数時間を保証しています。対策のレイヤーは異なりますが、処理時間から情報を漏らさないという原則は共通です。

📋 実装チェックリスト(チェックがつかなかった項目は要対応?)

  • ログイン処理でユーザーが存在しない場合と存在する場合のレスポンス時間が均一になっている
  • 認証失敗時のエラーメッセージがユーザー存在の有無を示していない(「メールアドレスまたはパスワードが正しくありません」等)
  • Laravel の Timebox 機構を独自の認証実装でバイパスしていない

事例2: パスワードリセットHostインジェクション(CVE-2017-9303)

影響バージョン 5.4.x(< 5.4.22)
CVSS 6.1(Medium)
参照 CVE-2017-9303GHSA-rc8x-jrrc-frfv修正commit

パスワードリセットメールのURL生成に $request->getHost() を使用していたため、攻撃者が Host ヘッダを偽装するとリセットリンクが攻撃者のドメインに向く脆弱性です。

問題点:「リクエスト情報は信頼できる」という前提のほころび

HTTPリクエストの Host ヘッダは、あくまでクライアントから送られてくるデータです。フォームに入力された値を検証するのと同じように、ヘッダ値も攻撃者が自由に操作できる前提で扱う必要があります。ところが、メールリンクのように「サーバー外部へ送信するURL」にリクエスト由来のホスト名をそのまま使うと、攻撃者にメールの中身を差し替えられる隙が生まれます。

想定される被害: 被害者が正規サイトだと思ってアクセスしたフィッシングサイトでパスワードリセットトークンを入力し、攻撃者にアカウントを乗っ取られる。

CVSS 6.1 の理由: 成立にはユーザー操作(攻撃者が誘導したリセットリンクを踏む)が必要なため Medium にとどまりますが、成功時の影響は大きいです。

修正diff:信頼できる設定値の使用に切り替え
<?php
// src/Illuminate/Auth/Notifications/ResetPassword.php
 public function toMail($notifiable)
 {
    return (new MailMessage)
        ->line('You are receiving this email because we received a password reset request for your account.')
-        ->action('Reset Password', route('password.reset', $this->token))
+        ->action('Reset Password', url(config('app.url').route('password.reset', $this->token, false)))
        ->line('If you did not request a password reset, no further action is required.');
 }

修正前は route() が Host ヘッダに基づくフルURLを生成していたため、攻撃者が Host ヘッダを偽装するとリセットリンクが攻撃者のドメインに向いていました。修正後は route() の第3引数に false を渡してパス部分のみを生成し、サーバー側の設定値 config('app.url') と結合するようにしています。

📋 実装チェックリスト(チェックがつかなかった項目は要対応?)

  • 外部送信するURL(メール内リンク等)に $request->getHost()$request->url() を使っていない
  • config('app.url') を正しい本番ドメインに設定している
  • プロキシ・ロードバランサー環境下で TrustProxies ミドルウェアの設定が適切になっている

A07: Authentication Failuresを克服するポイント

認証処理ではエラーメッセージの統一だけでは不十分で、処理時間の均一化まで踏み込む必要があります。また、Host ヘッダのようにクライアント由来のデータを「どこまで信頼してよいのか」の線引きも認証機能の品質を決める要素です。一見すると定型作業に見えるログインやパスワードリセットの実装ですが、「信頼できるデータとそうでないデータの境界(信頼境界)」を意識しないと、今回のような脆弱性が紛れ込みます。

A08: Software or Data Integrity Failures(ソフトウェアとデータの整合性の失敗)

2021年版から順位を変えず8位のカテゴリです。署名なしのソフトウェア更新、信頼できないソースからのプラグイン/ライブラリ、CI/CD パイプラインの改ざん、そして安全でないデシリアライゼーションなど、「コードやデータの完全性が検証されないまま信頼される」問題を扱います。

「復号できる=正しい」と勘違いしない

このカテゴリの中でも、アプリケーション開発者が踏みやすいのが「デシリアライゼーション」の落とし穴です。暗号化は「機密性」の保証であって「認証」ではありません。つまり、復号に成功したからといって、そのデータが正規のものであるとは限らないなど。この区別を見落とすと、暗号化されたデータを無条件に信頼する実装になり、重大な脆弱性を招きます。次の事例はまさにこの見落としから生まれたケースです。

事例3: X-XSRF-TOKENデシリアライゼーションRCE(CVE-2018-15133)

影響バージョン 5.5.x(< 5.5.40), 5.6.x(< 5.6.30)
CVSS 8.1(High)
参照 GHSA-qvqm-h22r-4cp9修正PR #25121

APP_KEY が漏洩した場合、X-XSRF-TOKEN ヘッダに細工したペイロードを送信することでリモートコード実行(RCE)が可能になる脆弱性です。Metasploit のモジュールが公開されているため攻撃のハードルは低いですが、CISA の Known Exploited Vulnerabilities カタログにも登録されており、実際に攻撃での悪用が確認されています。

問題点:decrypt() が復号後に自動で unserialize() していた

攻撃の流れは以下の通りです。

  1. /.env などから APP_KEY を入手する(第1回の事例2「.env ファイル情報漏洩」などが発端となる
  2. phpggc 等で PHP ガジェットチェーンのペイロードを生成する
  3. APP_KEY でペイロードを暗号化し、X-XSRF-TOKEN ヘッダとして送信する
  4. VerifyCsrfToken ミドルウェアが decrypt()unserialize() を実行して RCE に至る

問題の根本は、当時の Encrypter::decrypt() が復号後に自動で unserialize() を実行していたことです。unserialize() にユーザー入力を渡すのは PHP 公式マニュアルでも明確に警告されている危険な操作ですが、その前段に暗号化が挟まっていることで「鍵を知らない人には渡せない値=信頼できる」と誤認しやすい構造になっていました。実際には鍵さえ漏れれば迂回できるため、暗号化は「信頼できるデータの証明」にはなりません。

想定される被害: サーバー上で任意のコマンドが実行され、情報漏洩・データ改ざん・他システムへの侵入など広範な被害が発生する。

CVSS 8.1 の理由: ネットワーク越しに攻撃可能であり、成功時の影響範囲(機密性・完全性・可用性すべて)が大きい点が High として評価されています。

修正diff:デシリアライズの明示的な無効化
<?php
// src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php
 if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
-    $token = $this->encrypter->decrypt($header);
+    $token = $this->encrypter->decrypt($header, false);
 }
<?php[f:id:ak_tsukahara:20260429155919j:plain]
// src/Illuminate/Cookie/Middleware/EncryptCookies.php
+protected $serialization = [
+    'XSRF-TOKEN' => false,
+];

 // decryptCookie() メソッド
-protected function decryptCookie($cookie)
+protected function decryptCookie($name, $cookie)
 {
    return is_array($cookie)
        ? $this->decryptArray($cookie)
-        : $this->encrypter->decrypt($cookie);
+        : $this->encrypter->decrypt($cookie, $this->serialization[$name] ?? true);
 }

decrypt() の第2引数に false を渡すことで、復号時に unserialize() を実行しないように修正されました。

2つ目の diff についてもう少し詳しく見てみましょう。EncryptCookies ミドルウェアでは、$serialization プロパティに Cookie 名をキー、デシリアライズの要否を値としたマッピングを持っています。decryptCookie() が呼ばれると $this->serialization[$name] ?? true で Cookie 名に対応する値を参照し、XSRF-TOKENfalse が明示されているためデシリアライズが無効化されますが、それ以外の Cookie はデフォルト true(従来通りデシリアライズあり)となり、既存の動作との互換性が保たれています。XSRF-TOKEN は単なる文字列トークンであり、シリアライズされたオブジェクトとして復元する必要がないため、この変更で機能に影響はありません。

📋 実装チェックリスト(チェックがつかなかった項目は要対応?)

  • APP_KEY の管理はシークレットマネージャー(AWS Secrets Manager 等)経由にしている
  • .env ファイルへの Web アクセスが Nginx/Apache 等 の設定でブロックされている
  • unserialize() にユーザー入力・外部データを渡しているコードがない

A08: Software or Data Integrity Failuresを克服するポイント

PHP 公式マニュアルでも警告されている通り、unserialize() に信頼できないユーザー入力を渡すとオブジェクトインジェクション等の脆弱性につながる可能性があります。また、暗号化は「機密性」の保証であって「認証」ではないという点も見落としやすいポイントです。復号できることと内容が正しいことは別の問題であり、この区別を常に意識しておくことが大切です。

事例は単独で完結しない:攻撃チェーンの視点

本連載で扱った10件の事例は、単独で完結するものばかりではありません。特に意識しておく価値がある2つの攻撃チェーンを紹介します。

攻撃チェーン①:設定ミスから RCE へ

第1回の事例2(CVE-2017-16894:.env 漏洩)で APP_KEY を入手した攻撃者が、そのまま本記事の事例3(CVE-2018-15133:デシリアライゼーション RCE)に進める構造になっています。OWASP のカテゴリ上は A02 と A08 として独立して見えますが、実際の攻撃では滑らかに連鎖します。

攻撃チェーン②:認証情報の段階的な窃取

事例1(CVE-2022-40482:ユーザー列挙)で有効なアカウントを特定した後、事例2(CVE-2017-9303:Host インジェクション)でパスワードリセットトークンを窃取し、アカウント乗っ取りに至るルートが考えられます。

単体では CVSS が中程度の脆弱性であっても、組み合わせによって致命的な攻撃が成立するのが脆弱性連鎖の怖さであり、多層防御(defence in depth)が重要になる理由でもあります。

次回予告:第4回「取り上げていない5カテゴリとまとめ」(A01・A03・A06・A09・A10)

OWASP Top 10 は10カテゴリありますが、本連載が扱ったのは5つです。残り5つを意図的に外した理由と、連載全体を振り返りながら整理していきます。