0%

Chapter 4

Sessions That Hold

A Session Id Is a Key

After a login, the password is not sent again. The browser sends a with a , and whoever sends that id is ana as far as the server can tell. In with Python you made the id with secrets.token_urlsafe(16), which is 128 random bits, so nobody guesses one. This chapter is about every other way to end up holding a session id that works.

What is new in this chapter’s project

The project is your server from the end of Chapter 3 with one feature added. Here is what the new code does and which mistake it invites.

POST /prefs remembers a page size for a reader who has not logged in. To remember anything the server needs a session, so this route hands one out before login: a visitor session, whose user is None.

    elif segments == ["prefs"]:
        if method == "POST":
            response = prefs_error(request)
            if response is None:
                page_size = json_body(request)["page_size"]
                response = json_response(200, {"page_size": page_size})
                session = current_session(request, state)
                if session is None:
                    session_id = start_session(state, None, page_size=page_size)
                    response["headers"]["set-cookie"] = session_cookie(session_id)
                else:
                    session["page_size"] = page_size

A session is now {"user": ..., "page_size": ...}, and GET /me reports both: {"user": null, "page_size": 5} for a visitor who chose 5. Two helpers came with the feature: session_id_from(request) reads the id from the cookie, and current_session(request, state) returns the session that id names. sample_state() also has a "clock" entry for Lesson 3, and three are stubs that you write in Lessons 2, 3, and 5.

The login branch had to change as well, because a visitor who logs in already has a session. After a correct password it now runs these four lines:

                session_id = session_id_from(request)
                if session_id is None or session_id not in state["sessions"]:
                    session_id = start_session(state, None)
                state["sessions"][session_id]["user"] = user

When the request brings a cookie that names a session, the login keeps that session and writes the user into it. So the page size survives the login.

Read the four lines again and ask who chose the session id. The server did not choose it at this moment. The id arrived in the request, and the server only checked that it knows the id. An id that was handed to a visitor is then trusted with a logged-in user.

Which of the four supplied lines in the login branch gives a logged-in user to an id the server did not choose?

The attack, step by step

Three parties take part. The id is shortened to K7f...Qw.

attacker              victim's browser        server
   |                       |                   |
1  |-- POST /prefs --------------------------->|  new session K7f...Qw:
   |<-- set-cookie: session=K7f...Qw ----------|  user None, page_size 5
2  |-- plants K7f...Qw --->|                   |
3  |                       |-- POST /login --->|
   |                       |   cookie K7f...Qw |
   |                       |   ana's password  |
4  |                       |                   |  its user is now "ana"
5  |-- GET /me, cookie: session=K7f...Qw ----->|
   |<-- {"user": "ana", "page_size": 5} -------|
  1. The attacker sends POST /prefs and receives a session id.

  2. The attacker gets that id into the victim’s browser. The next section says how.

  3. The victim logs in with her own password. Her browser sends the planted cookie along.

  4. The server finds the session the cookie names and writes ana into it.

  5. The attacker sends GET /me with the id they have had since step 1, and the server answers as ana.

The same id travels in steps 1, 3, and 5. The attacker never guessed an id and never stole one. The server promoted a the attacker already had. This attack is called session fixation: the attacker fixes the id first, and the victim’s login makes it valuable.

This block acts the attack out on a plain . The id is readable on purpose; a real one is random.

Try it

It prints the victim's cookie now holds: id-the-attacker-knows and the attacker asks who that is: ana.

What does this print?

sessions = {"abc": {"user": None}}
sessions["abc"]["user"] = "ana"
print(len(sessions), sessions["abc"]["user"])

How does the id get into her browser?

Step 2 has two common routes.

  • A neighboring host. Say the reading lives at reading.example and a notes app at notes.reading.example. A page on the notes host can set a cookie for the whole domain reading.example, and a browser then sends that cookie to the reading list as well. Lesson 4 closes this route in the cookie itself.

  • A shared computer. The attacker visits the reading list first, notes the id, and leaves the cookie behind. The next person at that computer logs in with it.

Nothing on this site is a browser, so the course describes step 2 and does not show it: a real browser is what would carry the planted cookie. Every other step runs against your server in the next lesson.

A server that also accepted the session id from the would add a third route, a link with the id in it. This server reads the id from the cookie only.

Four ways to lose a session id

How the id is lostWhat it meansWhere it is handled
GuessedThe attacker tries ids until one worksClosed in HTTP with Python by an id from secrets
FixedThe attacker knows the id before it is worth anythingThis attack; Lesson 2
Read in transit or from a pageA copy is made while the cookie crosses a network without HTTPS, or by a script in a pageLesson 4
Never retiredA copy keeps working because the server never ends the sessionFinding F3; Lesson 3

A session id still works a year after its user last used it. Which of the four losses is that?

The repair, in one sentence

When what a session is trusted with changes, give it a new id. A login is such a change, and the old id, which someone else may know, must stop naming anything.

Try it

It prints the victim's new id is 22 characters the attacker never saw and the attacker asks who the old id is: None. The page size moved to the new session, and the old id names nothing.

The attacker never saw ana's password or the cookie her login returned. What did they know?

Nothing in your server has changed in this lesson. The next lesson runs the attack against it and closes it there.