Why Cursor Keeps Writing MD5 Password Hashes (CWE-328)

Dev.to / 4/27/2026

💬 OpinionDeveloper Stack & InfrastructureTools & Practical Usage

Key Points

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

TL;DR

  • AI editors default to MD5/SHA1 because training data is dominated by pre-2012 tutorials
  • MD5 cracks in hours on a consumer GPU -- this is not theoretical
  • Fix: bcrypt at rounds=12, or Argon2id for new projects

A few weeks ago a developer asked me to review his side project before pushing to production. Node.js backend, Cursor-built, clean architecture. The password storage looked like this:

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

MD5. In 2026. Not because the developer was careless -- because Cursor generated it and it worked.

I've now seen this in three separate projects over the past few weeks. Different developers, different apps, same pattern. The AI editors are consistent about it.

The Vulnerable Pattern (CWE-328)

// All three are wrong for password storage
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'); // Still wrong for passwords ❌

MD5 and SHA-1 are broken for collision resistance. But even SHA-256 is the wrong tool. The problem is speed. SHA-256 can hash billions of inputs per second on a GPU. That's exactly what makes it wrong for passwords.

A leaked database of MD5-hashed passwords is cracked in hours with a wordlist and a consumer GPU. SHA-256 is even faster, so it goes even faster.

Why AI Keeps Generating This

Training data volume. There are millions of StackOverflow answers, blog posts, and GitHub gists from 2008-2019 showing MD5 and SHA-1 for password hashing. bcrypt has been the right answer since around 2012, but old content drowns it out by sheer volume.

When you ask Cursor to "implement user registration," it pattern-matches against its training distribution. The most common association for "hash a password in Node.js" includes the crypto module with MD5 -- because that association appears millions of times in training data.

The model isn't broken. It's doing what it learned. The problem is what it learned from.

The Fix

const bcrypt = require('bcrypt');

// 12 rounds is the current recommended minimum
const hash = await bcrypt.hash(password, 12); // ✅

// Verification -- never compare raw strings
const isValid = await bcrypt.compare(inputPassword, storedHash); // ✅

For new projects, Argon2id is now OWASP's top recommendation:

const argon2 = require('argon2');

const hash = await argon2.hash(password); // Argon2id by default ✅
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)  # ✅

The cost factor matters. bcrypt at rounds=12 adds ~200-300ms per login check. Fine for users. For an attacker hashing billions of guesses, that latency scales linearly into years.

One Command to Check Your Codebase Now

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

If that returns results near any auth code, fix it before anything else ships.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with semgrep and gitleaks will catch most of what's in this post. The important thing is catching it early, whatever tool you use.