The model knows what a password column should look like. It also knows what bcrypt is. What it does not do, unless you ask, is wire the second thing into the first. So you end up with a users table that works — and a password column that reads like a phone book.
Cryptographic failures cover everything from storing passwords in plaintext to signing JWTs with 'secret' to leaving TLS off on an internal hop. The common thread is that something sensitive is sitting somewhere it should not be, in a form anyone who gets their hands on it can read. It is rarely about picking the wrong cipher — it is about not picking one at all.
What your AI actually built
You asked for user signup and login. The model gave you a clean auth flow: form, API route, database insert, session cookie. Every step looks right when you test it as yourself.
What you did not notice is that the password hit the database exactly as the user typed it. No hash, no salt, no pepper. The signing secret for your JWTs is the literal string 'secret' sitting in an env file that was committed last Tuesday.
Everywhere else, the pattern repeats. API keys stored in plain columns. Password reset tokens generated with Math.random. TLS terminated at the load balancer and then cheerfully proxied over HTTP inside the VPC. Each one looks fine on its own. Together they are a single compromised backup away from a headline.
How it gets exploited
An old database backup ends up on a misconfigured S3 bucket. The attacker does not need to exploit anything — they just list the bucket.
- 1Grab the dumpThey download users.sql. The password column is not hashed. Every credential is in the clear.
- 2Stuff the loginsThey replay the email and password pairs against Gmail, GitHub, and Stripe. Password reuse does the rest.
- 3Forge a sessionThe JWT signing secret is 'secret'. They mint an admin token in 30 seconds using jwt.io.
- 4Walk in the front doorThe admin token unlocks the dashboard, the billing page, and the impersonation endpoint. No password was ever cracked.
The attacker has a live admin session, every user credential, and a linked identity on half a dozen other services — all from a file that was never supposed to be sensitive.
Vulnerable vs Fixed
// app/api/auth/signup/route.ts
export async function POST(req: Request) {
const { email, password } = await req.json();
const user = await db.user.create({
data: {
email,
password, // goes in exactly as typed
},
});
return Response.json({ id: user.id });
}// app/api/auth/signup/route.ts
import { hash } from '@node-rs/argon2';
export async function POST(req: Request) {
const { email, password } = await req.json();
const passwordHash = await hash(password, {
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});
const user = await db.user.create({
data: { email, passwordHash },
});
return Response.json({ id: user.id });
}Two lines of setup and a rename. Argon2id is the current default — bcrypt is still fine if it is what you already have. The password the user typed never touches the database.
A real case
Adobe leaked 153 million password hints in plain text
The 2013 Adobe breach shipped a database where every password was encrypted with a single reversible key and every hint was plaintext — the textbook definition of cryptographic failure at scale.
Related reading
References
Find every place your secrets are hiding in plain sight.
Flowpatrol walks your app the way an attacker would and flags every credential, token, and header that is not where it should be.
Try it free