How to Hash Passwords Instead of Storing Them in Plaintext

Storing passwords in plaintext means one database breach hands an attacker every account immediately. The fix is to hash on registration and verify the hash on login - never compare raw strings. Here's how to do it correctly in a Flask/PostgreSQL app.

Security Engineerpythonflaskpostgresql

Why plaintext passwords are catastrophic

When you store a raw password string in a users table, any attacker who reads the database - via SQL injection, a backup leak, or a compromised admin - gets every user's password instantly. Worse, most users reuse passwords, so that breach propagates to their email, bank, and every other site.

The audit finding looks like this:

SELECT username, password FROM users;
-- username  | password
-- ----------+----------
-- alice     | hunter2
-- bob       | Password1

That's game over. Hashing makes a breach an inconvenience rather than a catastrophe.

Hash on register, verify on login

Use werkzeug.security - it wraps PBKDF2-HMAC-SHA256 with a per-user salt, which means two users with the same password get different stored strings.

from werkzeug.security import generate_password_hash, check_password_hash

# /register endpoint - hash before inserting
hashed = generate_password_hash(password)   # e.g. "pbkdf2:sha256:600000$..."
cur.execute(
    "INSERT INTO users (username, email, password) VALUES (%s, %s, %s)",
    (username, email, hashed),
)

On login, fetch the row by username only - never put the password in the WHERE clause - then verify in Python:

# /login endpoint - fetch by username, verify hash in Python
cur.execute(
    "SELECT id, username, password FROM users WHERE username = %s",
    (username,),
)
user = cur.fetchone()

if user and check_password_hash(user[2], password):
    return jsonify({"message": "Login successful", "user_id": user[0]})
else:
    return jsonify({"error": "Invalid credentials"}), 401

check_password_hash does a constant-time comparison, which prevents timing attacks. Never use == to compare hashes.

Why the login query changes too

The vulnerable pattern is:

SELECT id FROM users WHERE username = %s AND password = %s

This pushes the comparison into SQL, which has two problems: it breaks the moment you hash (the hash won't match the raw input), and it leaks timing information to anyone measuring query duration. Fetch by username, then call check_password_hash - the work stays in Python.

Production notes

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why is storing plaintext passwords dangerous?

Any attacker who reads the database - via SQL injection, a backup leak, or a compromised admin account - gets every user's password immediately. Because users reuse passwords, the breach spreads beyond your app to their email and other accounts.

How does generate_password_hash work?

werkzeug.security's generate_password_hash wraps PBKDF2-HMAC-SHA256 with a random per-user salt, then encodes the algorithm, iteration count, salt, and hash into a single string safe to store in a VARCHAR column. Each call produces a different string even for identical passwords.

Should I use bcrypt instead of werkzeug.security?

For new production systems, bcrypt or argon2id are the current recommendations because they are memory-hard and tunable. werkzeug.security's PBKDF2 is correct and safe, but bcrypt/argon2 are harder to attack with GPUs at high parallelism.

Is bcrypt safe for password hashing?

Yes. bcrypt is a deliberately slow, salted hash built for passwords - the cost factor makes brute force expensive and the per-password salt defeats rainbow tables. It is a solid choice alongside argon2 and scrypt; never store plain or fast-hashed (MD5/SHA-256) passwords.

Keep learning

Close a SQL Injection in a Search EndpointSecurity projectNeutralize a Stored XSS in CommentsSecurity projectRemove Hardcoded Credentials From Source CodeSecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →