コドモン Product Team Blog

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

LaravelのCVE修正diffで学ぶWebセキュリティ 第2回:入力値と暗号の落とし穴(A04・A05)

はじめに

前回(第1回)では「https://example.com/?--env=local というリクエストで環境を操作できる」「.env のAPP_KEYが漏れると致命的なリスクにつながる」という事例を通じ、A02(Security Misconfiguration)を読み解きました。

今回はA04(Cryptographic Failures)とA05(Injection)を取り上げます。どちらも「対策してあるはず」という思い込みが落とし穴になりやすいカテゴリです。「プリペアドステートメントを使っているからSQLインジェクションは大丈夫」「=== で比較しているから問題ない」——これらが本当に正しいのか、CVEのdiffを通じて確認してみましょう。

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

今回取り上げる事例

# CVE/識別子 カテゴリ
事例1 CVE-2017-14775 A04
事例2 CVE-2021-21263 A05
事例3 GHSA-4mg9-vhxq-vm7j A05
事例4 CVE-2021-43808 A05
事例5 CVE-2024-13918 A05

A04: Cryptographic Failures(暗号化の失敗)

2021年の2位から2025年では4位。かつては「Sensitive Data Exposure」という名称でしたが、2021年版でより根本的な原因に焦点を当てたカテゴリ名に変更されました。データが平文で送受信されていないか、弱いハッシュアルゴリズムを使っていないか、といった問題に加え、正しいアルゴリズムを使っていても実装上の不備でサイドチャネル情報が漏洩するケースもここに含まれます。

なぜ今も重要か

OWASP Top 10:2025では、防御策の一つとして「不必要に機密データを保存しない。……保持しないデータは盗まれない(Data that is not retained cannot be stolen)」と明記されています。不必要に保持しているデータは窃取の対象になり得ます。そのうえで、転送中・保存中を問わず、強力なアルゴリズムと実装で保護されているかを確認することが求められています。


事例1: remember_meトークンのタイミング攻撃(CVE-2017-14775)

影響バージョン < 5.5.10
CVSS 4.3(Medium)
参照 CVE-2017-14775PR #21320修正commit

Laravelの「ログイン状態を保持する」機能、いわゆる remember_me のトークン照合に使われていた === 演算子。一見すると普通の実装に見えますが、秘密情報の照合においては固有のリスクを孕んでいます。

// 修正前のコード: src/Illuminate/Auth/DatabaseUserProvider.php
public function retrieveByToken($identifier, $token)
{
    $user = $this->conn->table($this->table)->find($identifier);

    return $user && $user->remember_token && $token === $user->remember_token // ← ここがポイント
        ? $this->getGenericUser($user)
        : null;
}

問題点:=== は秘密情報の比較に使ってはいけない

等値比較に === を使うのは自然な実装です。ところが秘密情報の照合においては、その「自然な実装」がタイミング攻撃の入口になります

PHPの === は内部で memcmp を使って文字列を比較します。先頭バイトから順に照合し、最初に不一致を見つけた時点で即座に false を返す仕組みです。つまり、比較にかかる時間が「正解とどこまで一致しているか」に依存します。

攻撃者がトークンの先頭から1文字ずつ試し、レスポンスのわずかな時間差(ナノ秒〜マイクロ秒単位)を統計的に解析することで、正しいトークンを推測できてしまいます。これをタイミング攻撃(Timing Attack)と呼びます。Anthony Ferraraの研究によると、約49,000回の試行で15ナノ秒の差を検出可能とのことです。

想定される被害:任意ユーザーの remember_me トークンを推測して不正ログイン。

=== は秘密情報の比較に使ってはいけない

修正diff:定数時間比較への切り替え

// src/Illuminate/Auth/DatabaseUserProvider.php
 return $user && $user->remember_token
-    && $token === $user->remember_token
+    && hash_equals($user->remember_token, $token)
    ? $this->getGenericUser($user)
    : null;

hash_equals() はPHP 5.6で追加された関数で、2つの文字列が異なっていても常に同じ長さの時間をかけて比較する(定数時間比較)という特性を持ちます。先頭から不一致が見つかっても比較を打ち切らないため、タイミング差が生じません。

「WAFでレート制限しているから大丈夫では?」 という見方もあります。確かに、49,000回のリクエストを現実的に送りにくくする点でレート制限は有効な防御層です。ただし、===hash_equals() の変更は1行で完結し、パフォーマンスへの影響もほぼありません。「多層防御(defence in depth)」の観点から、コストの低い対策は積み重ねておく価値があります。

PHPのCSRF保護ミドルウェア(VerifyCsrfToken)が === でなく hash_equals() を使っている理由も同じです。秘密情報の比較においては、攻撃の糸口を与えない定数時間比較の実装が標準的な対策となります。

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

  • トークンやAPIキー、CSRFトークン等の秘密情報の比較に hash_equals() を使っている(=== を使っていない)
  • パスワードの照合に password_verify() を使っている(password_hash() 生成値には hash_equals() でなく password_verify() が正しい)
  • remember_me を実装する際、トークンの照合をフレームワーク任せにしていて独自実装していない

A05: Injection(インジェクション)

2021年の3位から2025年では5位。2021年版でXSS(クロスサイトスクリプティング)がこのカテゴリに統合され、SQLiだけでなくXSSやコマンドインジェクションなど幅広いインジェクション系の脆弱性を含むカテゴリになっています。

プリペアドステートメントが有効に機能するための前提条件を整理

OWASPはInjectionを「信頼できないデータがコマンドやクエリの一部としてインタープリタに送られること」と定義しています。プリペアドステートメント(バインディング)はSQLインジェクションへの有効な対策ですが、バインディングの仕組みが正しく機能するためには「入力値の型が期待通りである」ことが前提です。この前提が崩れるケースが次の事例です。


事例2: Query Builder配列インジェクションによるSQLインジェクション(CVE-2021-21263)

影響バージョン < 6.20.14, < 7.30.2, < 8.22.1
CVSS 7.5(High)
参照 GHSA-3p32-j457-pg5xPR #35865PR #35972

where() に配列を渡すと、バインディングが意図しない値にずれてしまう脆弱性です。以下のコードにおいて、リクエスト値が「単一の値」ではないケースを想定してみます。

php

// 攻撃者が id=1&id[]=2&id[]=3 のようなリクエストを送った場合
$id = $request->input('id'); // → ['2', '3'] という配列になり得る
User::where('id', $id)->first();

通常、 where('id', $id) でスカラー値を渡すと WHERE id = ? というプリペアドステートメントが生成され、 ?$id がバインドされます。ところが $id が配列だった場合、修正前のコードでは Arr::flatten() を使って配列を平坦化し、その先頭値をバインドしていました。これにより、複数のバインディング値がずれ、意図しないクエリが実行される可能性がありました。

意図しないクエリが実行される

修正diff:Flatten処理の除去とバリデーション強制

// src/Illuminate/Database/Query/Builder.php
 if (!$value instanceof Expression) {
-    $this->addBinding(is_array($value) ?
-        head(Arr::flatten($value)) : $value, 'where');
+    $this->addBinding(is_array($value) ?
+        head($value) : $value, 'where');
 }

初期修正では Arr::flatten() を除去し、配列の先頭値のみ取り出すようにしました。しかしネストされた配列(例: id[0][]=1)には不完全だったため、後続のPR #35972でさらにスカラー値のみを受け付けるよう厳格化されています。

プリペアドステートメントは強力な防御策ですが、特定の条件下ではその前提が崩れる場合があります。バインディングそのものが正しく動作するための前提条件(入力値の型)を検証しない限り、フレームワーク側でも想定外の動作を招くことがあります。HTTPリクエストにおいて「スカラーを期待しているパラメータに配列が来る」のはPHPの仕様上常に起こり得ます(id=1&id[]=2 の形式)。フレームワークの機能に依存しすぎず、アプリケーション層での型検証を組み合わせる設計が有効です。

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

  • $request->input() で受け取った値を where() に渡す前に型を検証している
  • where('column', $request->input('...')) のように、リクエスト値を直接クエリビルダに流し込んでいないか?
  • FormRequestのバリデーションルールでスカラー値を強制している(例: 'id' => 'required|integer'
  • intval()(int) キャスト等で、スカラーであることを明示的に保証している

事例3・4・5:その他のInjection事例

A05に含まれる残り3件を紹介します。事例3はSQLインジェクションの別パターン、事例4・5はXSS系の事例です。

事例3: SQL Server LIMIT/OFFSETのSQLインジェクション(GHSA-4mg9-vhxq-vm7j)

影響バージョン < 5.8.35, < 6.18.35, < 7.22.4
参照 GHSA-4mg9-vhxq-vm7j

SQL Serverドライバの SqlServerGrammar で、limit()offset() に渡した値がSQLに直接埋め込まれていました。MySQLやPostgreSQLでは ? プレースホルダーで処理されていたのに対し、SQL Server固有の実装で素通しになっていたものです。

修正は (int) キャスト4箇所の追加のみで完了しています。整数値として確定しているはずの LIMIT / OFFSET 句も例外ではなく、「このパラメータはユーザーから来ない」という思い込みが実装の漏れにつながる事例です。また、利用するDBドライバによって安全性が変わる点にも注意が必要です。

事例4: Blade @parent プレースホルダーXSS(CVE-2021-43808)

影響バージョン < 5.5.49, < 6.20.32, < 7.30.6, < 8.x-dev
参照 GHSA-66hf-2p6w-jqfw

Bladeの @parent ディレクティブは、内部処理でセクション名のSHA1ハッシュをプレースホルダーとして使っていました。SHA1は可逆でないとはいえ、セクション名が予測可能であれば攻撃者がプレースホルダーの値を事前に計算できてしまいます。

修正では、プレースホルダーの生成をコンパイル時から実行時に変更し、リクエストごとにランダムな値をソルトとしてSHA1ハッシュに付与するようになりました。内部実装の識別子であっても予測不可能性(unpredictability)を持たせることの重要性を示す事例です。

事例5: デバッグエラーページ Reflected XSS(CVE-2024-13918)

影響バージョン < 11.44.1, < 12.2.0
参照 GHSA-x8xh-g9g6-ff3rPR #53869

APP_DEBUG=true 時のエラーページでリクエストパラメータがHTMLエスケープされずに表示されていた脆弱性です。エラーページは「本番では使われない」という前提があったため、エスケープ処理が漏れていました。

この事例はA05(Injection/XSS)と同時に、デバッグモードを本番環境で有効にしたまま運用するという第1回のA02(Security Misconfiguration)とも交差する問題です。例外を設けず、すべての出力箇所で一貫した保護を適用することが防衛線の維持につながります。


A05: Injectionを克服するポイント

OWASPは防御策として、以下をはじめとする複数の対策を挙げています(主要な点を抜粋)。

クエリのインジェクション対策: プリペアドステートメント・パラメータ化クエリ・ストアドプロシージャなどのAPIを使い、入力値がインタープリタに直接渡らない設計にする。

入力値の検証: サーバーサイドでのポジティブリスト(許可リスト)検証を基本とし、型・形式・範囲が期待通りかを確認する。クライアント側の検証は補助的なものと捉え、頼り切らない。

出力のエスケープ: HTMLに出力する場合はHTMLエスケープ、SQLに埋め込む場合は適切なエスケープやバインディング、という「出力先に合わせたエスケープ」を徹底する。エラーページや内部ツールであっても例外を設けない。

事例2が示すように、バインディングの安全性は「入力値が期待通りの型である」という前提に支えられています。バインディングを使っているから大丈夫、ではなく、バインディングに加え、その前段での型検証を組み合わせることで、より堅牢なインジェクション対策となります。


次回予告:第3回「認証フローと整合性の前提を崩す攻撃」(A07・A08)

HTTPのHostヘッダは攻撃者が書き換えられる。APP_KEY が漏れると、なぜRCE(リモートコード実行)になるのか。認証フローと「信頼できるデータ」の前提が崩れる3件のCVEを読み解きます。