説明
MD5やSHA-1のような高速な汎用ハッシュでパスワードを保存すると、ハッシュを取得した攻撃者がGPUやクラウドを使って大量の候補を試せます。ソルトがない場合や、反復回数・メモリコストが低い場合はリスクが高まります。DBやバックアップの漏えい後にHashcatなどで解析され、元のパスワードの特定やアカウント乗っ取りにつながるおそれがあります。
想定される影響
- 特定されたパスワードでアカウントへ不正にログインされる可能性があります。
- 管理者アカウントを侵害されると、システムの制御権限を取得されるおそれがあります。
- アカウントへのアクセスを通じて機密データが漏えいする可能性があります。
- 特定したパスワードを他サービスで試す、クレデンシャルスタッフィングに悪用されるおそれがあります。
- 保管基準への適合やユーザーからの信頼が損なわれる可能性があります。
対処方法
- 新しい保存方式にはArgon2idを優先し、利用できなければscryptを検討してください。bcryptは既存システムとの互換性が必要な場合、PBKDF2は関連要件がある場合に使ってください。
- 現在の指針を基にコストを設定し、運用環境で測定してください。例はArgon2id
m≥19 MiB, t≥2, p=1、scryptN≥2^17, r=8, p=1、bcryptcost≥10、PBKDF2-HMAC-SHA256600,000回以上です。 - パスワードごとに一意のランダムソルトを使ってください。ライブラリが生成・保存する機能を持つ場合は、それを利用してください。
- ペッパーを使う場合は、パスワードDBとは別のシークレットストアやHSMで管理してください。ソルトや十分な計算コストの代わりにはなりません。
- ログイン成功後に、弱い既存ハッシュを現在のアルゴリズムと設定で再ハッシュしてください。
例
既存システムでbcryptを使い続ける比較です。bcryptはUTF-8入力の最初の72バイトだけを使うため、登録と検証で一貫した入力方針を適用し、長いパスワードを黙って切り詰めないでください。コスト12は例であり、デプロイ環境で負荷を測定する必要があります。
変更前
javascript
// 高速な汎用ハッシュ(MD5/SHA-1)でパスワードを保存
const crypto = require("crypto");
async function saveUserWeak(username, plainPassword, db) {
const digest = crypto
.createHash("md5")
.update(plainPassword, "utf8")
.digest("hex");
await db.users.insert({ username, passwordHash: digest });
}
変更後
javascript
// bcryptで一意のソルトと適切なコストを使用
const bcrypt = require("bcrypt");
async function saveUserSecure(username, plainPassword, db) {
const cost = 12; // コストの例。デプロイ環境で測定
const hash = await bcrypt.hash(plainPassword, cost); // ランダムソルトを内部で生成
await db.users.insert({ username, passwordHash: hash });
}
async function verifyLogin(username, inputPassword, db) {
const user = await db.users.findOne({ username });
if (!user) return false;
return bcrypt.compare(inputPassword, user.passwordHash);
}
解説:
- 変更前: MD5は高速な処理を目的としており、GPUやASICで多数の候補を試せます。ソルトや計算コストがないため、総当たり、辞書、事前計算テーブルを使う攻撃に弱くなります。
- 変更後: bcryptはパスワードごとにソルトを生成し、候補ごとの計算量を増やします。例ではコスト12で保存し、
bcrypt.compareで検証します。強いパスワード方針とログイン試行回数の制限も適用してください。