š 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
bcrypt
orArgon2
, 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 ?