
Laravelではバージョン8からPassword::uncompromised()を利用することで漏洩済みのパスワードを検知できます。
この記事では使い方や内部的な仕組みについて解説したいと思います。
uncompromisedの使い方
Passwordオブジェクトはバリデーションと組み合わせて使うことができます。 以下のようにリクエストのバリデーションメソッドに対してパスワードを検証することができます。
use Illuminate\Validation\Rules\Password; $request->validate([ 'password' => [ 'required', 'string', Password::min(8)->uncompromised(), // ← これ ], ]);
FormRequestでも利用することができます。
public function rules(): array
{
return [
'password' => [
'required',
'string',
Password::min(8)->uncompromised(),
],
];
}
また、漏洩数のしきい値を設定することも可能です。 以下のケースでは3件パスワード流出の検出がある場合は不合格とさせることができます。 デフォルト設定は1件でもNGという扱いになるのでここのしきい値調整はサービスによって検討する必要があります。
Password::min(8)->uncompromised(3);
uncompromisedの仕組み
なぜパスワードが流出しているかどうかを判別できるのでしょうか? uncompromisedは内部的にHave I Been Pwned(HIBP)のAPIを利用しています。
Have I Been Pwned(HIBP)とは
HIBPはメールアドレスやパスワードが過去にデータ漏洩事件に含まれていたかを検索することができるデータベース及びサービスです。 元Microsoft Regional DirectorであるTroy Hunt氏により運営されていて、FBIとも情報共有する等、信頼性の高いサービスです。
内部的な仕組み
HIBPのパスワード漏洩チェックはチェックしたいパスワードそのものを送信することはありません。
1️⃣パスワードをSHA-1でハッシュ化
$sha1 = sha1($password); // 例:5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD
2️⃣上位5文字だけHIBP APIに送信
GET https://api.pwnedpasswords.com/range/5BAA6
これにより、パスワード自体を外部に漏らさずに照合できます。(K-Anonymity)
3️⃣APIは一致する下位35文字のリストを返す
返却されるデータ例
1E4C9B93F3F0682250B6CF8331B7EE68FD:3303003 ...
4️⃣Laravelが完全一致するハッシュを探し、件数を取得
Laravelは「5BAA6」(先頭5文字)+「1E4C9B93F3F0682250B6CF8331B7EE68FD」(後ろ35文字)を結合して照合し、件数を確認します。
この例だと:の後ろが件数なので3303003件漏洩されていることになります。
※ちなみにLaravelではHIBPのAPI通信が失敗した場合、バリデーションはtrueとして扱われます。
運用上の注意点
HIBPのデータはあくまでも漏洩件数のため、パスワードの強度チェックではありません。パスワードの強度チェックが必要である場合は別途対応が必要です。
漏洩数のしきい値についてはHIBP公式としての明言はないようです。基本的にはデフォルト値である1つでも漏洩が認められる場合は禁止するべきですが、サービス設計によって検討したほうが良いかもしれません。
余談
NIST(米国国立標準技術研究所-National Institute of Standards and Technology-)の資料ではパスワードの設計について以下のように規定されています。
- 単要素パスワードを用いる場合は最低文字数は15文字以上を必須とする
- パスワードが多要素認証と組み合わせる場合、最低8文字以上でも可
- パスワードの最大長は64文字以上を受け付けることを推奨する
- パスワードに対して複雑性のルールの強制は設けてはならない
- パスワードの定期変更を義務付けてはならない
- パスワードの登録・変更時に「既知として弱い」「すでに漏洩の可能性がある」ものはブロックリスト方式で禁止するべき
このように、自身のWebサイトにパスワードログインを設ける場合はLaravelのPassword::uncompromised()を利用するのはもはや必須であると言えるかもしれません。