認証・認可の脆弱性:ログインとIDOR
応答時間からアカウントを推測する
この回でやること
文言もステータスコードも揃えたのに、存在するIDだけ応答が遅い。パスワードのハッシュ計算が生む時間差と、その消し方を扱います。
- 読む 約 7 分
- 最後にクイズ 1 問
このレッスンで学ぶこと
応答時間は、文言とステータスコードを揃えたあとにも残る、登録の有無が漏れる経路です。この回は、なぜ時間に差が出るのかと、その消し方を扱います。
読み終えると、20回ずつ送って中央値を比べる測り方で、この経路が塞がっているかを自分で確かめられるようになります。
揃えたのに、まだ分かる
練習用のショップで、文言を「利用者名またはパスワードが違います」に統一し、どちらも401を返すようにしました。そのうえで、同じリクエストを何度か送って時間を測ります。
プレーンテキスト
zzzz → 401 平均 12 ms
tanaka → 401 平均 240 ms本文もコードも同じなのに、20倍の開きがあります。1回では回線のばらつきに埋もれますが、20回ずつ送って中央値を取ると、差ははっきり残ります。
差が出る理由
パスワードは平文では保存しません。bcryptやArgon2のような関数でハッシュにして保存します。これらの関数はわざと遅く作ってあります。総当たりをする側の1回あたりの費用を上げるためで、100ミリ秒前後かかるのが普通です。
ここに素直な実装の落とし穴があります。
プレーンテキスト
1 users から名前で行を引く
2 行が無ければ → その場で 401 を返す ← 速い
3 行があれば → ハッシュを計算して照合する ← 重い
4 合わなければ → 401 を返す2で早く抜けると、3の重い処理を通りません。文言が同じでも、通った道が違うので時間が違います。時間差は文言の差と同じだけの情報を持っていて、しかも見た目には現れません。
消し方
行が見つからなくても、同じ重さの計算を通すのが基本です。
- 利用者が見つからないときも、あらかじめ用意した捨て札のハッシュに対して照合を1回走らせてから
401を返す - 早期リターンを書かない。行の有無で分岐した先が、どちらも同じ関数を1回呼ぶ形にする
「遅いほうに合わせて100ミリ秒待つ」は一見よさそうですが、本物の処理時間の揺れがその上に乗るので、回数を重ねると分布の形の違いから読めてしまいます。足すのではなく、同じ計算を通してください。
手を動かす
練習用の環境で、存在するIDと存在しないIDに同じでたらめなパスワードを20回ずつ送り、応答時間の中央値を比べてください。差が10ミリ秒以内に収まっていれば、この経路は塞がっています。