I asked Cursor to add a signup endpoint to a side project last week. Clean code, worked first try. Then I read the part where it stored the password: one line, hashlib.md5
. No salt. No work factor. Just a raw MD5 digest sitting in my users table.
I have seen this enough times now that I stopped being surprised. Claude Code does it. Copilot does it. Ask any AI editor to "hash the password before saving" and there is a real chance you get MD5 or SHA-1 back.
import hashlib
def store_password(password):
return hashlib.md5(password.encode()).hexdigest()
Sometimes it is SHA-1 instead. Sometimes SHA-256. It does not matter. They are all built to be fast, and speed is exactly what you do not want here.
The training data. MD5 password hashing was standard advice on StackOverflow and in PHP tutorials for the better part of a decade. Millions of examples. The models learned the pattern because the pattern was everywhere. They do not know that in 2012 LinkedIn leaked 6.5 million unsalted SHA-1 passwords and most were cracked within days. They just know that "hash a password" and "md5" show up together a lot.
The math is the real problem. A modern GPU rig computes billions of MD5 hashes per second. If your user database leaks, and databases leak, every common password falls almost instantly. No salt means identical passwords produce identical hashes, so one rainbow table cracks your whole table at once.
Use a slow hash built for passwords: bcrypt, scrypt, or argon2id. They are deliberately expensive to compute, which barely matters for a single login but wrecks an attacker running billions of guesses.
import bcrypt
def store_password(password):
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
def verify_password(password, stored):
return bcrypt.checkpw(password.encode(), stored)
bcrypt salts automatically, so you never manage it yourself. rounds=12
is a sane default in 2026. If you want the current best practice, argon2id via the argon2-cffi
library is the OWASP-recommended choice.
Install and you are done:
pip install bcrypt
The catch: your AI editor will not flag the MD5 version. It is valid Python. It runs. It passes your tests. Nothing tells you it is wrong until the breach.
I have been running SafeWeave for exactly this. It hooks into Cursor and Claude Code as an MCP server and flags weak crypto like md5/sha1 password hashing before I move on to the next file. That said, even a basic pre-commit hook with semgrep will catch most of it. The important thing is catching it while you still have the context, whatever tool you use.