CursorがMD5パスワードハッシュを書き続ける理由(CWE-328)

Dev.to / 2026/4/27

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical Usage

要点

  • AIエディタ(Cursorなど)は、学習データに古いチュートリアルが多いため、パスワード保存でMD5/SHA1のような古いハッシュ関数を自動生成しがちだと指摘されています。
  • MD5は一般的な消費者向けGPUでも数時間で解読できるため、「理論上の問題」ではなく実害につながり得ます。
  • 推奨される対策は、bcryptでrounds=12に設定すること、または新規プロジェクトならArgon2idを採用することです。
  • 具体例として、Node.jsバックエンドでcrypto.createHash('md5').update(password).digest('hex')の形が複数の独立したプロジェクトで繰り返し見つかったという実例が紹介されています。

TL;DR

  • AIエディタは、学習データが2012年以前のチュートリアルに支配されているため、デフォルトでMD5/SHA1を使いがちです
  • MD5は一般的なGPUで数時間でクラック可能です -- 机上の空論ではありません
  • 修正: bcrypt(rounds=12)を使う、または新規プロジェクトではArgon2idを使います

数週間前、開発者が本番環境へ投入する前に彼のサイドプロジェクトをレビューしてほしいと私に頼みました。Node.jsのバックエンドで、Cursorで作成、クリーンアーキテクチャ。パスワードの保存はこのようになっていました:

const hash = crypto.createHash('md5').update(password).digest('hex');
await db.query('INSERT INTO users (email, password) VALUES ($1, $2)', [email, hash]);

MD5。2026年です。開発者が不注意だったわけではありません -- Cursorが生成して、しかも動いたからです。

私はこの状況を、ここ数週間で3つの別々のプロジェクトで見かけました。開発者は別、アプリも別。でもパターンは同じ。AIエディタはそれに対して一貫しています。

脆弱なパターン(CWE-328)

// 3つともパスワード保存としては誤り
crypto.createHash('md5').update(password).digest('hex');    // CWE-328 ❌
crypto.createHash('sha1').update(password).digest('hex');   // CWE-328 ❌
crypto.createHash('sha256').update(password).digest('hex'); // パスワードならやはり誤り ❌

MD5とSHA-1は、衝突耐性(collision resistance)のために破綻しています。しかし、SHA-256でさえも正しい道具ではありません。問題は「速さ」です。SHA-256はGPU上で毎秒数十億件の入力をハッシュできます。まさにそれが、パスワードには不適切である理由です。

MD5でハッシュされたパスワードが流出した場合、単語リストと一般的なGPUで数時間でクラックされます。SHA-256はさらに高速なので、なおさら早く進みます。

なぜAIはこれを作り続けるのか

学習データ量。2008〜2019年のStackOverflowの回答、ブログ記事、GitHubのgistには、パスワードハッシュにMD5やSHA-1を使っている例が何百万もあります。bcryptは2012年頃から正しい答えでしたが、古いコンテンツが圧倒的な量によってそれを押し流しています。

Cursorに「ユーザー登録を実装して」と頼むと、学習時の分布に対してパターン照合を行います。「Node.jsでパスワードをハッシュする」について最も一般的に結び付くものには、MD5とセットでcryptoモジュールが含まれます -- なぜなら、その関連付けが学習データに何百万回も登場するからです。

モデルが壊れているのではありません。学習した内容をその通りに実行しているだけです。問題は、「学習した内容が何なのか」です。

修正

const bcrypt = require('bcrypt');

// 12ラウンドは現在の推奨する最低値です
const hash = await bcrypt.hash(password, 12); // ✅

// 確認 -- 生の文字列同士は決して比較しない
const isValid = await bcrypt.compare(inputPassword, storedHash); // ✅

新規プロジェクトでは、Argon2idは現在OWASPの最上位の推奨です:

const argon2 = require('argon2');

const hash = await argon2.hash(password); // デフォルトでArgon2id ✅
const isValid = await argon2.verify(hash, inputPassword); // ✅

Python:

import bcrypt
hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))  # ✅
is_valid = bcrypt.checkpw(input_password.encode(), hash)  # ✅

コスト係数は重要です。rounds=12 の bcrypt は、ログイン照合 1 回あたり約200〜300ms を追加します。ユーザーにとっては問題ありません。しかし攻撃者が何十億もの推測をハッシュするなら、このレイテンシは直線的に増えて、年単位にまで伸びます。

今すぐコードベースをチェックするための「ワンコマンド」

grep -r "createHash" --include="*.js" --include="*.ts" --include="*.py" | grep -iE "md5|sha1"

これで認証コードの近くに結果が返ってきたら、他のことが始まる前に直してください。

私はこれのために SafeWeave を使っています。これは Cursor と Claude Code を MCP サーバーとしてフックし、次に進む前にこれらのパターンにフラグを立てます。ただし、semgrep と gitleaks を使った基本的な pre-commit フックでも、この投稿に書かれている内容の大半は見つかります。重要なのは、どのツールを使うにせよ、早い段階でそれを見つけることです。