OWASP Top 10:2025 — A04 Cryptographic Failures for AI-Generated Code

Written by the Rafter Team

We ran Rafter against a batch of week-old vibe-coded SaaS apps last month. Every single one that did its own password hashing used something wrong — md5, sha256 with no salt, or a homebrew "encrypt" function that was just base64. None of the founders knew. The LLM had written hashlib.md5(password.encode()).hexdigest() because that's what twenty years of Stack Overflow answers taught it to write, and nobody read the line before shipping it.
What A04 actually is
A04 covers anything where you mishandle data that's supposed to stay secret: passwords, API keys, session tokens, PII, payment data. The category is broad on purpose. Storing a Stripe key in config.js and committing it counts. Hashing passwords with SHA-1 counts. Serving the login page over HTTP counts. Using AES in ECB mode (the one where identical plaintext blocks produce identical ciphertext blocks — the famous Tux penguin image example) counts. The concrete failure you'll hit first is almost always one of two things: a secret committed to git, or a password hashed with something that GPUs can crack at 100 billion guesses per second.
Why AI-generated code trips on it
LLMs are trained on a corpus where md5 and sha1 outnumber argon2 and bcrypt by an order of magnitude, and where every tutorial has a placeholder API_KEY = "sk-..." sitting in source. They will happily reproduce both patterns unless you stop them. The specific failures we see most often in AI-generated code:
- Password hashing with a general-purpose digest.
hashlib.sha256,crypto.createHash('sha256'), sometimes stillmd5. These were never designed for passwords. You wantbcrypt,scrypt, orargon2id. - Secrets pasted into source. The model needs a placeholder to make the snippet runnable, so it inlines
OPENAI_API_KEY = "sk-proj-..."instead of reading from env. Then the founder fills in the real key and commits. Math.random()for tokens, IDs, password resets. It's not cryptographically random. Reset tokens generated this way are guessable.- Custom "encryption" wrappers that XOR or base64 something and call it encrypted.
- TLS skipped in fetch/axios calls —
rejectUnauthorized: falseto make a "connection error" go away.
Next.js and FastAPI scaffolds are usually fine by default. The trouble shows up the moment you ask the model to "add a password reset flow" or "let users upload an API key" — that's when the bad primitives appear.
The fix on agentic CLIs (Claude Code, Codex)
Don't ask the model to "make it secure." That's too vague and you'll get theater. Be specific about which primitive you want. A prompt that actually works:
Audit this repo for A04 Cryptographic Failures. Specifically:
1. Replace any password hashing that isn't argon2id, bcrypt, or scrypt.
2. Find any hardcoded secrets (API keys, DB URLs, JWT secrets) and move
them to .env, add .env to .gitignore, and add a .env.example.
3. Replace Math.random() for any security-sensitive value (tokens, IDs,
reset codes) with crypto.randomBytes / secrets.token_urlsafe.
4. Flag any AES usage that's ECB mode or missing an IV.
Show me the diff before applying.
The "show me the diff before applying" line is the important one. Read the diff. If a secret was already committed, rotating the key is the fix — removing it from HEAD does nothing because it's still in git history. Run git log -p -- path/to/file and assume anything you see is burned.
Rafter's Copy-for-AI button on each finding produces a prompt scoped to exactly one vulnerability with the file, line, and the specific remediation. Paste it into Claude Code and it'll apply a targeted fix instead of rewriting your whole auth module.
The fix on opinionated platforms (base44, Greta, OpenClaw)
You usually can't reach into the password-hashing function on a hosted builder. What you can do is tell the platform's agent what guarantees you need, in language the platform understands. Three things to ask for, in order:
- "Use the platform's built-in authentication. Don't write a custom password hasher." Every serious AI-app builder has a managed auth integration (Supabase Auth, Clerk, Auth0, the platform's native auth). Use it. If the agent generated a
userstable with apasswordcolumn, that's a red flag — push it to delete the column and wire up managed auth instead. - "Move every API key and secret to the platform's secrets manager / environment variables, not the project source." Name the keys explicitly — Stripe, OpenAI, database URL, JWT signing secret. If the platform shows you a "code view" or "files" panel, scan it once for anything that looks like
sk-,pk_live_, or a long random string. - "All external HTTP calls must use HTTPS, and any user-uploaded files containing secrets must be encrypted at rest." Most platforms do this; some don't. Asking forces the agent to verify rather than assume.
The constraint to accept: you're trusting the platform's crypto, not auditing it. That's a reasonable trade if you pick a platform that takes auth seriously and a bad trade if you don't. Check the platform's security page before you build anything that touches payments or PII.
See also
- A02 Security Misconfiguration — committed secrets and default credentials live in the same neighborhood.
- A07 Authentication Failures — password hashing sits at the boundary of A04 and A07; the auth post covers session and token handling.
- A08 Software or Data Integrity Failures — signing, verification, and the other half of "use real crypto primitives."