Fairy
Resources

How to Avoid: Passwords Stored Without a KDF

August 1, 2026 · 8-minute read · Fairy

The short answer

To prevent passwords stored without a KDF in AI-generated code, replace any plaintext, MD5, or SHA1 password storage with a slow key derivation function like bcrypt, argon2, or scrypt. These algorithms are designed to resist brute-force attacks by being computationally expensive. Always use timing-safe comparison when verifying passwords to prevent timing attacks.

How to Avoid: Passwords Stored Without a KDF

To prevent passwords stored without a KDF in AI-generated code, you need to identify any use of plaintext storage, MD5, SHA1, or other fast hashing algorithms and replace them with a slow key derivation function: bcrypt, argon2, or scrypt. These KDFs are specifically designed to resist brute-force attacks by being computationally expensive. When AI generates password handling code, it frequently defaults to fast hashes that appear in its training data but are trivially cracked in a breach.

This is one of the most dangerous authentication bugs AI produces, and it often ships to production unnoticed because the code "works" — passwords get stored and verified. The problem only becomes catastrophic when your database leaks.

Why AI Generates This Bug

AI models generate insecure password storage for a specific reason: their training data is full of it.

MD5 and SHA1 appear in millions of code samples, tutorials, Stack Overflow answers, and legacy applications. When you ask an AI to "hash a password," it reaches for patterns it's seen most frequently. The statistical likelihood of generating crypto.createHash('md5') or hashlib.sha1() is high because these patterns are overrepresented in training corpora.

The model doesn't understand that MD5 was deprecated for security purposes in 2004. It doesn't know that SHA1 collisions have been publicly demonstrated. It sees "hash" and "password" and produces what it's seen before.

This creates a particularly insidious failure mode: the code looks correct. It hashes the password. It stores something that isn't plaintext. A developer skimming the output might assume security has been handled. The vulnerability only materializes when an attacker gets database access and cracks every password in hours.

What Makes Fast Hashes Dangerous

The difference between MD5 and bcrypt isn't academic — it's the difference between passwords cracking in seconds versus centuries.

Modern GPUs can compute MD5 hashes at rates exceeding 50 billion per second. SHA1 runs at around 15 billion per second. SHA256 is slower but still achieves billions per second on consumer hardware.

At these speeds, an attacker with a leaked database can:

Slow KDFs like bcrypt, argon2, and scrypt change this equation entirely. They're designed to take 100-300 milliseconds per hash on purpose. They use memory-hard algorithms that resist GPU parallelization. They include a configurable work factor that lets you increase difficulty as hardware improves.

The result: instead of cracking a password in milliseconds, each guess takes hundreds of milliseconds. A billion-guess attack becomes computationally infeasible.

How to Detect This in AI-Generated Code

Search your codebase for these patterns near password handling:

JavaScript/Node.js

// DANGEROUS - fast hash
const crypto = require('crypto');
const hash = crypto.createHash('md5').update(password).digest('hex');
const hash = crypto.createHash('sha1').update(password).digest('hex');
const hash = crypto.createHash('sha256').update(password).digest('hex');

Python

# DANGEROUS - fast hash
import hashlib
hash = hashlib.md5(password.encode()).hexdigest()
hash = hashlib.sha1(password.encode()).hexdigest()
hash = hashlib.sha256(password.encode()).hexdigest()

Java

// DANGEROUS - fast hash
MessageDigest md = MessageDigest.getInstance("MD5");
MessageDigest md = MessageDigest.getInstance("SHA-1");
MessageDigest md = MessageDigest.getInstance("SHA-256");

PHP

// DANGEROUS - fast hash
$hash = md5($password);
$hash = sha1($password);
$hash = hash('sha256', $password);

Any of these patterns used for password storage is a critical vulnerability. The fix isn't to add salt to these fast hashes — it's to replace them entirely with a KDF.

The Correct Pattern: Using bcrypt

bcrypt is the most widely supported KDF and the right default choice for most applications.

Node.js with bcrypt

const bcrypt = require('bcrypt');

// Storing a password
async function hashPassword(plaintext) {
  const saltRounds = 12; // Work factor - increase over time
  const hash = await bcrypt.hash(plaintext, saltRounds);
  return hash; // Store this in your database
}

// Verifying a password
async function verifyPassword(plaintext, storedHash) {
  const match = await bcrypt.compare(plaintext, storedHash);
  return match; // true if password is correct
}

Python with bcrypt

import bcrypt

# Storing a password
def hash_password(plaintext: str) -> bytes:
    salt = bcrypt.gensalt(rounds=12)  # Work factor
    return bcrypt.hashpw(plaintext.encode(), salt)

# Verifying a password
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
    return bcrypt.checkpw(plaintext.encode(), stored_hash)

Key implementation details

  1. Use the library's comparison function: bcrypt.compare() and bcrypt.checkpw() are timing-safe. Never compare hashes with === or ==, which leak information through timing differences.

  2. Don't add your own salt: bcrypt generates and embeds its own salt in the hash output. Adding a separate salt provides no additional security and complicates your code.

  3. Store the full output: bcrypt's output includes the algorithm identifier, work factor, salt, and hash. Store the entire string; you need it all for verification.

Alternative: Using argon2

argon2 won the Password Hashing Competition in 2015 and is considered the state of the art. It offers better GPU resistance than bcrypt through memory-hardness.

Node.js with argon2

const argon2 = require('argon2');

// Storing a password
async function hashPassword(plaintext) {
  const hash = await argon2.hash(plaintext, {
    type: argon2.argon2id,  // Recommended variant
    memoryCost: 65536,      // 64 MB
    timeCost: 3,            // 3 iterations
    parallelism: 4          // 4 threads
  });
  return hash;
}

// Verifying a password
async function verifyPassword(plaintext, storedHash) {
  return await argon2.verify(storedHash, plaintext);
}

Python with argon2

from argon2 import PasswordHasher

ph = PasswordHasher(
    time_cost=3,
    memory_cost=65536,
    parallelism=4
)

# Storing a password
def hash_password(plaintext: str) -> str:
    return ph.hash(plaintext)

# Verifying a password
def verify_password(plaintext: str, stored_hash: str) -> bool:
    try:
        ph.verify(stored_hash, plaintext)
        return True
    except:
        return False

argon2 is the better choice for new applications if your stack supports it. bcrypt remains an excellent choice with broader library support.

Common Mistakes in the Fix

Even when developers know to use bcrypt or argon2, AI-generated code often makes these errors:

Mistake 1: Using a work factor that's too low

// TOO WEAK - cost of 4 is trivially fast
const hash = await bcrypt.hash(password, 4);

// BETTER - cost of 12 takes ~300ms per hash
const hash = await bcrypt.hash(password, 12);

A work factor of 4 defeats the purpose of using bcrypt. Start at 12 and benchmark on your production hardware.

Mistake 2: Rolling your own comparison

// DANGEROUS - timing attack vulnerability
if (storedHash === await bcrypt.hash(inputPassword, storedHash)) {
  // authenticated
}

// CORRECT - use the library's timing-safe comparison
if (await bcrypt.compare(inputPassword, storedHash)) {
  // authenticated
}

String comparison with === returns faster when characters don't match, leaking information about the correct hash. The library's comparison function takes constant time.

Mistake 3: Hashing on the client

// DANGEROUS - password available in browser console, network logs
const hash = await clientSideBcrypt(password);
await fetch('/api/login', { body: JSON.stringify({ hash }) });

// CORRECT - send password over HTTPS, hash on server
await fetch('/api/login', { body: JSON.stringify({ password }) });

Client-side hashing doesn't improve security. The hash becomes the password — anyone with the hash can authenticate. Always hash server-side over HTTPS.

How to Catch This Before Production

Manual review catches some instances, but authentication bugs are systematic. AI models will keep generating insecure password storage because their training data hasn't changed.

Effective detection requires:

  1. Automated scanning for fast hash functions near password variables
  2. Review of all authentication flows by someone who knows the patterns
  3. Testing that verifies bcrypt/argon2/scrypt is actually in use

This is precisely what Fairy for Code handles. Every AI-generated authentication flow gets reviewed for this pattern specifically. Our reviewers confirm passwords use a slow KDF with appropriate work factors and timing-safe comparison.

For a quick check on your current code, Fairy Scout provides free AI PR review that flags this vulnerability pattern.

Beyond Password Storage

Fixing password hashing is necessary but not sufficient for secure authentication. AI-generated code frequently contains other authentication vulnerabilities that compound the risk:

JWT tokens signed without expiration mean a leaked token works forever. Protected routes missing auth checks expose data without any authentication. OAuth state not validated enables login CSRF attacks.

These patterns appear in the same codebases where password storage is broken. A comprehensive authentication review covers all of them.

The Real Risk Calculation

The question isn't whether your database will ever leak. Breaches happen to organizations of every size through countless vectors: SQL injection, backup exposure, insider access, supply chain attacks, misconfigured cloud storage.

The question is what happens when it does.

With MD5-hashed passwords, the answer is: every user's password gets cracked within hours, enabling credential stuffing attacks across every service where they reused that password.

With bcrypt at cost factor 12, the answer is: attackers face years of computation per password. They'll crack a few weak passwords from dictionary attacks. Strong passwords remain protected.

The code change is small. The risk reduction is enormous. The only reason not to do it is if you didn't know — and now you do.

Summary

AI generates insecure password storage because its training data contains millions of examples using MD5 and SHA1. The fix is straightforward: use bcrypt, argon2, or scrypt with appropriate work factors and timing-safe comparison.

Detect the problem by searching for fast hash functions near password handling. Fix it by switching to a KDF. Prevent recurrence by reviewing AI-generated authentication code before it reaches production.

If you're deploying AI-generated code that handles authentication, get started with Fairy to ensure these patterns get caught systematically, not accidentally.

Frequently asked questions

Why does AI generate code that stores passwords with MD5 or SHA1?

AI models learn from training data that includes legacy codebases where MD5 and SHA1 were common. These algorithms appear frequently in tutorials and older documentation, so models reproduce them without understanding that they're cryptographically broken for password storage.

What is a KDF and why is it required for passwords?

A Key Derivation Function (KDF) is a cryptographic algorithm designed to be slow and memory-intensive. For passwords, KDFs like bcrypt, argon2, and scrypt make brute-force attacks impractical by requiring significant computation per guess, unlike fast hashes like MD5 which can be computed billions of times per second.

How do I detect weak password hashing in my codebase?

Search for imports or calls to MD5, SHA1, SHA256 with password-related variable names. Look for crypto.createHash(), hashlib.md5(), or MessageDigest.getInstance('MD5') near password handling. The fix is replacing these with bcrypt.hash(), argon2.hash(), or equivalent KDF calls.

Is SHA256 safe for password storage?

No. While SHA256 is cryptographically secure for integrity verification, it's too fast for password storage. Modern GPUs can compute billions of SHA256 hashes per second, making brute-force attacks feasible. Always use a slow KDF designed for passwords.

What work factor should I use for bcrypt?

Start with a cost factor of 12 for bcrypt, which provides good security while keeping login times under 300ms on modern hardware. Increase the factor as hardware improves. For argon2, use at least 64MB memory and 3 iterations as a baseline.


Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.

More resources