When you ask an AI to "add a login system" without specifying the hashing algorithm, there is a real chance it stores passwords in plain text or with MD5. The developer tests the login, it works, they move on. They never looked at the database. Here is how to check what your app is actually storing.
Why it matters
Databases get breached. When a breach happens, everything in the database is exposed. If passwords are stored as plaintext, every user's password is immediately readable. If they are stored with MD5 or SHA1, they are crackable with a modern GPU in minutes to hours for most common passwords.
If they are stored with bcrypt or Argon2 at an appropriate cost factor, a breach still exposes the hashes, but cracking them is computationally expensive enough that users have time to change their passwords before most are compromised. The damage from a breach is still bad, but the timeline is different.
How to check your database right now
Look at a password field directly. Connect to your database and run a query to inspect one stored password (use a test account you created yourself):
-- PostgreSQL: look at a stored password
SELECT email, password FROM users WHERE email = 'test@yourdomain.com' LIMIT 1;
-- MySQL equivalent:
SELECT email, password FROM users WHERE email = 'test@yourdomain.com' LIMIT 1;What you might see, and what each one means:
Plaintext password
Example: hunter2 or mypassword123
Critical issue. Any database access exposes all user passwords immediately.
MD5 hash
Example: 5f4dcc3b5aa765d61d8327deb882cf99 (32 hex characters)
Critical issue. Modern GPUs crack most MD5 password hashes in seconds to minutes.
SHA1 or SHA256 without salt
Example: 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 (40 hex characters for SHA1)
High risk. Fast to crack, especially if unsalted (rainbow tables apply).
bcrypt hash
Example: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewdBPj3oRvC.kF8S
Good. Starts with $2b$ (or $2a$). The 12 is the cost factor.
Argon2 hash
Example: $argon2id$v=19$m=65536,t=3,p=4$...
Excellent. The current OWASP recommendation for new applications.
If it is wrong: how to fix it
You cannot decrypt an MD5 hash back to the original password and rehash it. The fix requires a migration during which existing users are re-hashed on their next login:
// Node.js: bcrypt migration during login
import bcrypt from 'bcrypt';
async function login(email: string, plainPassword: string) {
const user = await db.user.findUnique({ where: { email } });
if (!user) throw new Error('Invalid credentials');
// Check if password is using old algorithm (MD5 in this example)
if (isLegacyMD5Hash(user.password)) {
const md5Hash = createMD5(plainPassword);
if (md5Hash !== user.password) throw new Error('Invalid credentials');
// User authenticated with old hash - upgrade to bcrypt
const newHash = await bcrypt.hash(plainPassword, 12);
await db.user.update({
where: { id: user.id },
data: { password: newHash }
});
return user;
}
// Normal bcrypt verification
const valid = await bcrypt.compare(plainPassword, user.password);
if (!valid) throw new Error('Invalid credentials');
return user;
}After enough users have logged in, you can force a password reset for any accounts still using the old hash format and disable the legacy check entirely.
For new code: use bcrypt with cost factor 12
// Node.js with bcrypt
import bcrypt from 'bcrypt';
// Hashing (at registration/password change)
const SALT_ROUNDS = 12;
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
await db.user.create({ data: { email, password: hashedPassword } });
// Verification (at login)
const isValid = await bcrypt.compare(plainPassword, user.password);
// ---
// Node.js with Argon2 (preferred for new applications)
import argon2 from 'argon2';
const hashedPassword = await argon2.hash(plainPassword, {
type: argon2.argon2id,
memoryCost: 65536, // 64MB
timeCost: 3,
parallelism: 4,
});
const isValid = await argon2.verify(hashedPassword, plainPassword);If you use a managed auth provider
If your app uses Clerk, Auth0, Supabase Auth, or Firebase Authentication, the password hashing is handled for you and uses appropriate algorithms. You do not need to implement this yourself. The issue arises when authentication is built from scratch or when an AI generated the auth code without specifying the hashing requirements.
$ check --password-hashing
If you want to know what your app is actually storing, I can check the password hashing while looking at the rest of the auth code too.
$ ./request-security-audit.sh →