Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Password hashing and session security

After this lesson: I can store passwords and check sessions safely in code.

60 min lesson3 commandsSQL injection lab: attack a weak shop search, fix the query and prove the fix

A stolen table should not hand over passwords

Password hashing means storing a one-way scrambled copy of each password instead of the password itself. It matters because a stolen table of plain passwords opens every account at once, and people reuse passwords on other sites.

A session is the other half. After sign-in, the site gives the browser a token to show on every later request. A token that can be guessed, or never ends, works like a stolen password.

By the end of this lesson

Note

I can store passwords and check sessions safely in code.

Key words

WordWhat it means
Hasha fixed-size scramble of data that cannot be turned back
Saltrandom text added to a password before it is hashed
Slow hasha hash made slow on purpose, to hinder guessing
Session tokena random value that stands for a signed-in visitor
Idle timeoutending a session after a quiet spell
Absolute lifetimethe longest a session may last, however busy

The cells students fill in are left blank in this preview.

Store a salted slow hash, never the password

A hash turns a password into a fixed-size scramble that cannot be reversed. The site stores the hash. At sign-in it hashes what was typed and compares the two.

A salt is random text added before hashing, different for every user. Two users with the same password get different hashes. A slow hash takes a fraction of a second on purpose, so guessing millions of passwords takes far too long.

Worked example: what a thief can do

How the password is storedWhat the thief seesGuesses the thief can try
Plain textEvery passwordNone needed
Fast hash, no saltSame hash for same passwordBillions a second, all users at once
Fast hash with a saltA different hash for each userBillions a second, one user at a time
Slow salted hashA different hash for each userA few thousand a second, one user at a time

The cells students fill in are left blank in this preview.

The same password, hashed twice

Code: python
import hashlib
import secrets
# Change password to "tulip2" and press Run. Equal passwords still give different hashes.
password = "tulip"
salt_one = secrets.token_bytes(16)
salt_two = secrets.token_bytes(16)
first = hashlib.pbkdf2_hmac("sha256", password.encode(), salt_one, 100000)
second = hashlib.pbkdf2_hmac("sha256", password.encode(), salt_two, 100000)
again = hashlib.pbkdf2_hmac("sha256", password.encode(), salt_one, 100000)
print(len(first), len(second))
print("different salts:", first == second)
print("same salt:", first == again)

A common mistake

Warning

Many people think hashing a password with one fast function is enough. It is not. A fast hash lets an attacker try billions of guesses a second. Use a slow, salted password hash such as scrypt, bcrypt or Argon2, from a trusted library.

See that a salt changes the whole hash

The next two lines hash two invented words with a fast hash, so you can see the effect. The second word is the first with three characters added at the front, standing in for a salt. Compare the first characters of the two answers.

A hash of the word tulip

Command
printf 'tulip' | sha256sum

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

The same word with a salt in front

Command
printf 'q7Ktulip' | sha256sum

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

How the shop adds its users

Command
docker exec app grep -n "INSERT INTO users" /tmp/app/app.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

What the three answers tell you

The first hash starts 6a6b4652 and the second starts 56b6ba29. Three added characters changed every part of the answer. That is why a salt defeats a table of ready-made hashes.

The third prints line 20 of the shop's program. It inserts users into a column named password, and the values are the passwords as typed. Ask what a stolen copy of that table would give an attacker.

A session token must be random and secret

A session token is what the browser shows on every request after sign-in. Anyone holding it is treated as that visitor.

So it must be long, random and made by a secure generator, never from a counter or the clock. The server keeps the real session, with the user and the times. The cookie carries only the token.

Worked example: flags on the session cookie

FlagWhat it doesWhat it helps stop
HttpOnlyHides the cookie from page scriptsA script stealing the token
SecureSends the cookie only over an encrypted linkSomeone on the network reading it
SameSiteWithholds the cookie on requests from other sitesA hostile page acting as the visitor

The cells students fill in are left blank in this preview.

How long is a random token?

Code: python
import secrets
# Change 16 to 32 and press Run. The token gets longer.
token = secrets.token_urlsafe(16)
print(len(token))
print(token == secrets.token_urlsafe(16))

Sessions must end

A session left open for ever is a spare key left in the door. Give it two limits. An idle timeout ends it after a quiet spell. An absolute lifetime ends it after a set time, however busy.

Signing out must destroy the session on the server, not only in the browser. Give a new token at sign-in so an old one cannot be reused.

Worked example: three sessions at the same moment

StartedLast seenNowResult
09:0009:2009:40Fine: 20 minutes idle, limit is 30
09:0009:1009:50Idle: 40 minutes quiet
09:0016:5017:10Too old: 8 hours 10 minutes, limit is 8 hours

The cells students fill in are left blank in this preview.

Judging a session with two limits

Code: python
IDLE = 30
LIFETIME = 480


def state(created, last_seen, now):
    if now - created > LIFETIME:
        return "too old"
    if now - last_seen > IDLE:
        return "idle"
    return "ok"


# Change 50 to 40 and press Run. Then try 500.
print(state(0, 10, 50))

About this lab and its source: SQL injection lab: attack a weak shop search, fix the query and prove the fix

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.