0%

Sessions That Hold · capstone

Capstone project: Project: Sessions That Hold

Three lessons made three repairs: a new id at login, two limits, and a with its flags. Each has its own test. This project reads them as one policy, adds the one piece that is still missing, and proves the whole policy with five tests.

The policy

QuestionAnswer on the reading
When does a start?When a visitor saves a preference, and at every login, always under a new id from secrets
What does it hold?user, page_size, created, last_seen
What moves?last_seen, on every request that finds the session live
When does it end?After IDLE_LIMIT seconds without a request, ABSOLUTE_LIMIT seconds after it started, at logout, and at login, when a new session replaces it
What does the cookie say?__Host-session, with Secure, HttpOnly, SameSite=Lax, Path=/, and no Domain
What may a cache keep?Nothing about a session: every response that sets a cookie, and every session route, says no-store

Every row except one is finished. The gap is in “When does it end?”.

The session nobody comes back to

current_session removes an expired session when its id is used. A visitor who never returns never uses the id, so the entry stays in state["sessions"] for as long as the server runs. And POST /prefs hands a session to anyone who asks.

sweep_sessions(state) closes the gap. It reads the time from state["clock"](), removes every session for which session_expired(...) is true, and returns how many it removed. start_session calls it first, so the store is cleaned whenever a session starts.

One detail decides how to write it. Removing an entry from a while a for reads the same dict raises RuntimeError: dictionary changed size during iteration as soon as the loop takes its next step. So sweep in two passes: first collect the expired ids in a list, then remove them in a second loop over that list.

Why does sweep_sessions first collect the expired ids in a list and remove them in a second loop?

Milestones

1. A busy session still ends. Write test_attack_busy_session_still_ends(). It is the thief of Lesson 3 who sends a request every ten minutes. Log in under a fixed clock that starts at 1000. Then repeat: add 600 to clock["now"], ask GET /me, and assert that the user is ana. Keep going while the next step is still short of the limit:

    while clock["now"] + 600 < 1000 + reading_server.ABSOLUTE_LIMIT:

After the loop the session is 28200 seconds old. Add 601, which puts it one second past the limit, ask again, and assert that the user is None.

2. Logout ends the session on the server. Write test_attack_logout_ends_the_session_on_the_server(). Log in with steps.login_cookie, ask GET /me and assert ana, send POST /logout with the cookie, then ask GET /me with the same cookie and assert None. The attacker is anyone who kept a copy of the cookie from before the logout.

Run python -m pytest -q. It ends with 13 passed: both new tests pass the first time. Lesson 3 already built the absolute limit, and with Python built a logout that removes the session. These two tests do not prove a repair. They guard one, and the quiz below says against what.

Your logout test passes the first time you run it. What is it for?

3. The sweep. Write sweep_sessions(state) and call it as the first line of start_session. No test of yours changes color: python -m pytest -q still ends with 13 passed.

4. Watch the policy. Run the sessions scenario:

python pl_sec.py sessions

It logs in, lets 31 minutes pass on a clock it owns, asks again, then logs in and out and tries the old cookie. The ids are shortened here:

--- a session that is left alone ---
ana logs in                          POST /login   -> 200
set-cookie: __Host-session=<id>; HttpOnly; Max-Age=28800; Path=/; SameSite=Lax; Secure
ana asks who she is                  GET /me       -> 200  {"user": "ana", "page_size": 20}
(the clock moves 31 minutes)
the same cookie, 31 minutes later    GET /me       -> 200  {"user": null, "page_size": 20}
--- a session that is logged out ---
ana logs in again                    POST /login   -> 200
ana logs out                         POST /logout  -> 204
set-cookie: __Host-session=""; HttpOnly; Max-Age=0; Path=/; SameSite=Lax; Secure
the cookie from before the logout    GET /me       -> 200  {"user": null, "page_size": 20}
--- result ---
after 31 idle minutes the session answered as: nobody
after logout the old cookie answered as: nobody

5. The findings. In SECURITY.md, check that rows F3, F4, and F5 each name a test and say closed.

What is still open

A sweep bounds how long a dead session stays in the store. It does not bound how many live visitor sessions a stranger can open in half an hour: each POST /prefs without a cookie creates one, and none of them expires for 30 minutes. A real service adds a cap, or waits to create a session until it has something worth remembering. The sweep also reads the whole store each time a session starts, so the same flood makes every POST /prefs slower. This course names both problems and builds neither fix.

Then press Check my work. Your sessions now hold. The bearer tokens do not: they are still two fixed in the source that never expire, and Chapter 5 replaces them.

Task

Finish the policy.

  1. In tests/test_attacks.py, write test_attack_busy_session_still_ends() and test_attack_logout_ends_the_session_on_the_server() as the lesson describes. Both use steps.login_cookie(state, "ana", "correct horse"). The busy test runs under helpers.fixed_clock(...), asks GET /me every 600 seconds and asserts ana each time while the next step is still short of reading_server.ABSOLUTE_LIMIT after the login, then moves the clock to one second past that limit and asserts None. The logout test asserts ana before the logout and None after it, with the same .

  2. In reading_server.py, write sweep_sessions(state): read the time from state["clock"](), remove every session for which session_expired(...) is true, and return how many you removed.

  3. Call sweep_sessions(state) as the first line of start_session.

  4. Run python -m pytest -q (13 tests pass) and python pl_sec.py sessions.

  5. In SECURITY.md, make sure rows F3, F4, and F5 each name a test and say closed.

Then press Check my work. It repeats the checks of Lessons 2, 3, and 4 with other clock times, ids, and page sizes: the two limits and the clock first, then the cookie’s name and flags, then the new id at login and no-store. It calls sweep_sessions on a store with one live, one idle, and one too-old session, which must return 2 and keep the live one, and calls start_session on such a store, which must sweep it; a sweep that builds a new of the live sessions is accepted. It passes a login through http_adapter as bytes, without opening a , and reads the set-cookie and cache-control lines that come back. It runs your five tests of this chapter by name against servers it puts in place of reading_server.handle and reading_server.sample_state: all five must pass against a correct server; each must fail against the one mistake it is for (a login that keeps the id, no idle limit, no absolute limit, a logout that keeps the session, a cookie without Secure); and all five must fail against a server that refuses every login. Last, it reads rows F3, F4, and F5 of SECURITY.md. Nothing from your is graded.