👉 If you haven’t read it yet, and know little about login implementations, check out this article first: Why a Slow Login Could Save You from a Legendary Sh*tstorm… or how to implement login the right way
Storing passwords on your website without hashing them is like keeping cash in a shoebox. But even if you do use hashes, there are a few chef tricks every good dev should apply: salt and pepper.
Yes, like in the kitchen.
And no, it’s not some far-fetched metaphor. Let’s go 👇
Let’s say you’re using MD5 as your hashing algorithm (which you absolutely should NOT do, as I explained in this brilliant post 👉 Why a Slow Login Could Save You from a Legendary Sh*tstorm… or how to implement login the right way).
In your database, you’d have a table of hashed passwords.
The plain password column should never exist, obviously — but I’m including it here just to make the example clearer.
| user_id | password (not in DB) | hash (pasword MD5) |
| 1 | 123456 | e10adc3949ba59abbe56e057f20f883e |
| 2 | password123 | 482c811da5d5b4bc6d497ffa98491e38 |
| 3 | 123456 | e10adc3949ba59abbe56e057f20f883e |
This method has two major problems:
- Users who use the same password end up with the exact same hash.
Result: if an attacker figures out user 1’s password, they automatically know user 3’s too. Party time 🎉. - Since you’re generating the MD5 hash directly from the password, anyone can just go to a reverse lookup site (like md5decrypt.net) and crack the hash in seconds. You’re basically handing over the keys.
And to fix both issues… we use salt and pepper.
🧂 What is salt in cryptography?
A salt is a random value added before hashing a password. It’s like adding a secret ingredient before cooking, so the dish never tastes the same — even if you use the exact same base.
In this case, the salt is a unique value for each user, and the hash is calculated like this:
HASH = MD5(password + salt)
👉 Important: the salt needs to be stored in the database, because without it, you wouldn’t be able to verify the login.
That means your table now needs an extra column:
| user_id | password (not in DB) | salt | hash (MD5 of pass + salt) |
| 1 | 123456 | oRmjVGj15 | 40d04a9e37a3aa567ad2ea40072a4641 |
| 2 | password123 | ORrgKHDjF | e9abff604fc0ad2bcdf3308840329686 |
| 3 | 123456 | oLDpQn2dQ | d96fcafb2c6d838b6ab4ce1831d39305 |
Now both of the previous problems are gone:
💡 Even if two users have the same password, their hashes will be completely different — thanks to each one having their own unique salt.
So even if an attacker steals the entire database, they can’t just do a simple “lookup and match” using a list of known hashes (rainbow tables).
In fact, they’d need a separate table for every possible salt… which means infinite tables. Not even trendy AI or gamer-grade GPUs can scale that 💸.
🤔 But wait… if someone steals the database, wouldn’t they also get the salt?
Yep. And if they also know which algorithm you’re using (spoiler: they will), they could try to crack a specific hash like this:
- Grab a dictionary of common passwords (
123456,password,qwerty, etc). - Try them one by one, concatenated with the corresponding salt.
- Hash the result and compare it to the one in the table.
- If it matches… bingo 🎯.
But here’s the good news:
- They have to do this user by user. No more getting a dump of the database and get thousands of accounts at once.
- Rainbow tables are useless in this scenario.
- And if you’re using a slow algorithm like
bcryptorArgon2, the whole brute-force process gets painfully slow. So slow the hacker might as well give up and start mining Dogecoin instead 🐶💰.
👉 Conclusion (for now)
Yes, it’s still technically possible to attack. But it’s no longer trivial — you’re throwing a lot of obstacles in the villain’s way.
And if you want even more security… 🌶️ It’s time to add some pepper.
🌶️ Let’s add the pepper
Pepper is another secret ingredient — but with one important difference: it’s not stored with the hash.
Or more precisely: it’s not stored in the database.
It’s a secret key, fixed for the whole application, that you add to the password in addition to the salt.
Now the hash is calculated like this:
HASH = MD5(password + salt + pepper)
And let’s say our pepper is:
pepper = UCFl1PVFpz53
Now our hash table would look like this:
| user_id | password (not saved) | salt | hash (MD5 of pass + salt + pepper) |
| 1 | 123456 | oRmjVGj15 | 919e36790a0b48a67d7e8c10dbbeb4fa |
| 2 | password123 | ORrgKHDjF | 78cebca31441aa00ff026a78d22d0b09 |
| 3 | 123456 | oLDpQn2dQ | 99c09fe497f7daae0351db7ebc0f8a35 |
Now, even if an attacker steals your entire database, they can’t do ANYTHING with it if they don’t have the pepper. Not even with brute-force attempts.
Alright, but if I don’t store the pepper in the database… where do I keep it?
Well, brace yourself — because just like we said a slow login has its perks, the pepper should live in an environment config file. Yes, a file. As in… hardcoded.
But for the love of all digital chefs: don’t leave the pepper shaker on the table. Meaning: don’t stick the pepper in your code and push it to GitHub like it’s no big deal. Don’t be that person. Protect it — even just a little.
🧠 In summary: checklist to avoid looking ridiculous
If you’re going to store passwords, do it with style:
- ✅ Use a strong algorithm like
bcrypt,scrypt, orArgon2 - ✅ Generate a unique, random salt for each user
- ✅ Add pepper (and store it outside the database)
- ✅ And never, EVER use MD5 or SHA1 — not even if your boss says so.
And read this post for more solid advice 👉 Why a Slow Login Could Save You from a Legendary Sh*tstorm… or how to implement login the right way
🤔 What if I don’t want to complicate things?
Good call — because all of this has already been invented.
Just use a library that handles it for you. Pretty much any serious framework already does all this under the hood. But hey, at least now you know why it does.
Did you enjoy this spicy little security lesson? Then share it with that one friend who’s still storing passwords in plain text — let’s see if it gives them a bit of heartburn.

So, what do you think ?