2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Laravel の token guard 認証バイパス、表に無い 11 以下はどうなの?全タグのソースを確認してみた

2
Posted at

はじめに

セキュリティ関連対応に追われている @___nix___ です。

背景

2026/10/19 に Laravel からこんなアドバイザリが出ました。

MySQL / MariaDB 環境で token guard を使っていると、API トークンを知らなくても既存ユーザーとしてログインできてしまう、という Critical なやつです。

影響範囲は < 12.68.0 と < 13.29.0 で、修正版は 12 と 13 しか載っていません。
範囲には下限が無いので 11 以下も数字の上では含まれますが、じゃあ 11 以下は修正版があるの?全部ダメなの?と聞かれて、答えられなかったので調べてみました。

概要

先に答えを書いておきます。

11 以下も同じコードを持っています。11 系は v11.56.1 でこっそり(?)修正されていて、10 系以下では修正を確認できませんでした。

調べたのは laravel/framework のソースだけです。実際に攻撃が通るかは試していないので、「コード上こうなっている」という話として読んでくださいね。

詳細

問題の箇所は src/Illuminate/Auth/TokenGuard.php です。11.x の修正コミットを見るとこうなっています。

$ git show 5056ca5379 -- src/Illuminate/Auth/TokenGuard.php
@@ -81,7 +81,7 @@ class TokenGuard implements Guard
 
         $token = $this->getTokenForRequest();
 
-        if (! empty($token)) {
+        if (is_string($token) && ! empty($token)) {
             $user = $this->provider->retrieveByCredentials([
                 $this->storageKey => $this->hash ? hash('sha256', $token) : $token,
             ]);
@@ -122,7 +122,7 @@ class TokenGuard implements Guard
      */
     public function validate(array $credentials = [])
     {
-        if (empty($credentials[$this->inputKey])) {
+        if (! is_string($credentials[$this->inputKey] ?? null) || empty($credentials[$this->inputKey])) {
             return false;
         }

修正前は「空でなければ OK」だったので、user() で hash を使っていない場合と validate() では、配列のような文字列以外の値も検索条件に渡っていました。
標準の provider(Eloquent / Database)は値が配列だと whereIn を組み立てるので、MySQL / MariaDB の緩い型変換と組み合わさると、トークンと関係ない行に一致してしまう、という流れのようです。

修正は user() と validate() の両方に is_string を足しただけ。1行ずつです!

調査

1. 修正がどのタグから入ったか

13・12・11 の各ブランチに同じ修正コミットがありました。含まれる最初のタグはこちら。

$ git tag --contains 5056ca5379
v11.56.1
v11.57.0

13 系は v13.27.0、12 系は v12.68.0 からです。どれも 2026/08/25 のリリースでした。

13 系はアドバイザリだと >= 13.29.0 が修正版ですが、コード上は v13.27.0 で既に直っています。(何故?)
理由は分からなかったので、更新するなら素直に 13.29.0 以上にしておきましょう。

2. 古い系列の最新タグ

10 系の最新は v10.48.29…と思いきや、2026/08/11 に v10.50.3 が出ていました!9 系も同じ日に v9.52.22 が出ています。
なのでここに修正が入っていないかも見ておきます。

$ for t in v11.56.0 v11.56.1 v10.50.3 v9.52.22 v8.83.29 v7.30.7 v6.20.45; do
>   printf '%-10s ' "$t"; git show "$t:src/Illuminate/Auth/TokenGuard.php" | grep -F 'empty($token)' | head -1
> done
v11.56.0           if (! empty($token)) {
v11.56.1           if (is_string($token) && ! empty($token)) {
v10.50.3           if (! empty($token)) {
v9.52.22           if (! empty($token)) {
v8.83.29           if (! empty($token)) {
v7.30.7            if (! empty($token)) {
v6.20.45           if (! empty($token)) {

修正の2週間前にリリースされた 10 / 9 系にも入っていません。惜しい!

3. 全タグを総なめ

最新タグだけでは途中の版が分からないので、5〜11 系の正式リリースのタグ(ベータ等は除く)を全部見てみます。

$ cat sweep.sh
for t in $(git tag | grep -E '^v?(5|6|7|8|9|10|11)\.[0-9]+\.[0-9]+$' | sort -V); do
  src=$(git show "$t:src/Illuminate/Auth/TokenGuard.php" 2>/dev/null) || continue
  if grep -qF 'is_string($token)' <<< "$src"; then echo "$t fixed"; else echo "$t no-check"; fi
done | awk '{split($1, v, "."); sub(/^v/, "", v[1]); c[v[1] " " $2]++} END {for (k in c) print k, c[k]}' | sort -V

$ bash sweep.sh
5 no-check 276
6 no-check 123
7 no-check 81
8 no-check 154
9 no-check 106
10 no-check 124
11 fixed 2
11 no-check 109

※ TokenGuard が無い 5.1 以前は continue で飛ばしています。

11 以下で今回の型チェックを持つのは v11.56.1 と v11.57.0 の2つだけでした!(取得した時点の正式タグでの話です)

5 系の 276 個には注意があって、provider が whereIn を使うようになったのは v5.6.26 からです。v5.2.0〜v5.6.25 の 192 個は型チェックが無いのは同じですが、検索の組み立て方が違うので、今回と同じ流れで通るかは見ていません。

範囲 タグ数 状態
v11.56.1 / v11.57.0 2 今回の型チェックあり
v11.0.0〜v11.56.0 109 今回と同じ経路あり
6〜10 系(全版) 588 今回と同じ経路あり・修正確認できず
v5.6.26〜v5.8.38 84 今回と同じ経路あり
v5.2.0〜v5.6.25 192 型チェック無し・経路は未評価

4. hash を有効にしていれば安心?

token guard には 'hash' => true の設定があって、user() ではトークンを hash('sha256', ...) に通します。PHP 8 なら配列を渡した時点で TypeError になるので、user() 側は止まります。

ところが validate() は hash の設定を見ずに、受け取った値をそのまま provider に渡しています。
validate() は true / false を返すだけなのでログインまではしませんが、アプリ側でこの結果を使って認証の判断をしている場合、hash の設定はここでは効かず、文字列以外の値が provider に渡る経路が残ります。実際に影響が出るかは DB やアプリの使い方次第で、ここでは試していません。

あと、TokenGuard には strict_types の宣言が無いので、整数などのスカラー値は hash() に渡すと文字列に変換されます。文字列以外が全部 TypeError で弾かれるわけではないですね。

対応

まず config/auth.php で 'driver' => 'token' の guard を探しましょう。Sanctum や Passport だけなら、この件の TokenGuard は通りません。

使っていたら、系列ごとにここまで上げます。

11 系: 11.56.1 以上
12 系: 12.68.0 以上
13 系: 13.29.0 以上

10 系以下は修正版が見つからなかったので、修正の入った系列への移行を考えることになります。

すぐ上げられない場合は、TokenGuard が認証する前に、guard の input_key(既定は api_token)がクエリ・フォーム・JSON のどこから来ても「あるなら文字列であること」を確認しておきましょう。認証の middleware より後で見ても間に合いません。
validate() を自分で呼んでいる箇所があれば、渡す値にも同じチェックを入れておきます。両方を確実に塞ぐなら、やっぱり更新が一番ですね。

アドバイザリの範囲は < v12.68.0 と書かれているので、修正済みの 11.56.1 / 11.57.0 も範囲上は含まれてしまいます。composer audit で引っかかっても、TokenGuard のコードを見れば修正済みかどうか確認できます。(実際の composer audit の出力は確認していません)

終わりに

表に載っていない 11 は 11.56.1 からちゃんと型チェックが入っていて、10 以下の正式タグでは同じ修正を確認できませんでした。
アドバイザリの表だけで判断せず、手元のバージョンのソースを一度覗いてみると安心ですね。

もっと良い調べ方がある方は是非教えてくださいませ。

一言

今の勢いだと何をやっておくというのも大事ですが、それ以上に「狙われない」ことの方が重要になってる気がしますね。狙われたら最後って気がしてます。(本来はダメなんですけどね)

もし気に入って頂けたらここでのいいね&フォローとX( @___nix___ )でのフォローもお願い致します。(少し元気を貰えますw)

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?