Sessions That Hold · practice
Cookie Flags on Purpose
Your server ends its
session=<id>; HttpOnly; Max-Age=3600; Path=/
Those attributes leave a browser free to do three things with the cookie. It may send it over plain HTTP, where anyone on the network path can read it. It may send it along with a form that a page on another site posts to the reading
Four instructions
A cookie attribute is an instruction to the browser that stores the cookie. This lesson sets four of them on purpose.
| Attribute | What it asks of a browser |
|---|---|
Secure | Send me only over HTTPS. |
HttpOnly | Do not show me to scripts in a page. |
SameSite=Lax | When a page on another site starts the request, send me only if the browser is loading a whole page here, as when the user follows a link, and the request is a GET. A form that another site posts, an image it loads, and a request its script makes all go without me. |
Path=/ and no Domain | I belong to this exact host, and to every path on it. |
“Another site” has a precise meaning. A site is the scheme plus the registrable domain, the part of a host name that someone registered, here reading.example. So https://notes.reading.example and https://reading.example are the same site. That is why SameSite does nothing about the neighboring host of Lesson 1. The prefix in the next section does, and Chapter 8 takes up requests from the same site.
SameSite has two other values. Strict would also keep the cookie back when a reader follows a link to the list from another site, so they would arrive logged out. This service does not choose it. None asks for no protection at all.
A page on another site causes a request to the reading list. With SameSite=Lax, when does a browser attach the cookie?
The __Host- prefix
The cookie’s name can carry a rule too. A browser refuses to store a cookie whose name starts with __Host- unless the cookie has Secure, has Path=/, has no Domain, and arrived over HTTPS. A cookie that a page on notes.reading.example sets for the whole domain needs a Domain attribute, so it can never be called __Host-session. No neighboring host can plant a cookie of that name for the reading list.
The server does its part by reading only that name. Once SESSION_COOKIE is "__Host-session", a cookie called session means nothing to session_id_from, whoever planted it.
A page on notes.reading.example tries to set __Host-session=evil; Domain=reading.example; Secure; Path=/. What does a browser do?
An exception retired
HTTP with Python left Secure off, in Lesson 7.1, because its server spoke plain HTTP on your own computer. That
pl_sec.py still talks plain HTTP to your server on Secure cookie back. Some browsers make an exception for a server on your own computer, which is for development only. The tool is not a browser: it builds the Cookie header by hand, and from this lesson on it says so right under its first line:
pl_sec sends the __Host-session cookie by hand; a browser would not send it over plain HTTP
Who obeys
The server only writes instructions. A test can check that they are written. Whether they are followed is up to the browser, and no browser checks these flags anywhere on this site. A program that is not a browser can send any Cookie header it likes, over any connection. Chapter 8 writes the part of a browser that decides when a cookie is sent.
Your test passes: the Set-Cookie value has every flag. What has the test shown?
The cookie you are aiming for
With max_age defaulting to ABSOLUTE_LIMIT, a browser forgets the cookie when the server’s absolute limit would end the session anyway. Once the flags are set, a login answers with:
__Host-session=<id>; HttpOnly; Max-Age=28800; Path=/; SameSite=Lax; Secure
SimpleCookie writes the attributes in alphabetical order, whatever order you set them in, and a flag such as Secure is a bare word with no value.
What does this print?
import http.cookies
cookie = http.cookies.SimpleCookie()
cookie["__Host-session"] = "abc"
cookie["__Host-session"]["path"] = "/"
cookie["__Host-session"]["secure"] = True
cookie["__Host-session"]["samesite"] = "Lax"
print(cookie["__Host-session"].OutputString())
Logout needs no change. The cookie that ends a session comes from the same session_cookie("", max_age=0), so it has the same name and flags. It has to: a browser ignores a __Host- cookie that lacks them, and that includes the one that deletes.
Write the attack first
The attack is a browser doing what the bare cookie allows, so the test reads the instructions. Log in as ana through reading_server.handle(...), assert status 200, and take the text in response["headers"]["set-cookie"]. Assert that it starts with __Host-session=, that Secure, HttpOnly, SameSite=Lax, and Path=/ are in it, and that Domain is not. Run python -m pytest -q: it ends with 1 failed, 10 passed.
Your task
In
tests/test_attacks.py, writetest_attack_session_cookie_carries_its_flags()and watch it fail.Set
SESSION_COOKIE = "__Host-session".In
session_cookie, setsecuretoTrueandsamesiteto"Lax", the same wayhttponlyandpathare set, and change the defaultmax_agetoABSOLUTE_LIMIT. Set no domain.In row F4 of
SECURITY.md, name your test and set the status toclosed in 4.4.
Run python -m pytest -q again. It ends with 11 passed. Your two earlier tests of this chapter needed no change, because steps.cookie_pair never spells the cookie’s name.
Then press Check my work. The three repairs of this chapter are in place. The next lesson puts them together as one policy and deals with the sessions nobody comes back to.
Task
Write a test that reads the
In
tests/test_attacks.py, writetest_attack_session_cookie_carries_its_flags(). Log in as ana throughreading_server.handle(...), assert status 200, and takeresponse["headers"]["set-cookie"]. Assert that thestarts with __Host-session=, that it containsSecure,HttpOnly,SameSite=Lax, andPath=/, and that it does not containDomain. Runpython -m pytest -q: this test must fail first.In
reading_server.py, setSESSION_COOKIE = "__Host-session".In
session_cookie, addsecure(set it toTrue) andsamesite(set it to"Lax"), and change the defaultmax_agetoABSOLUTE_LIMIT. Do not set a domain.Run
python -m pytest -q: all 11 tests pass. Your two earlier tests of this chapter did not need a change, becausesteps.cookie_pairnever spells the cookie’s name.In row F4 of
SECURITY.md, name your test and set the status toclosed in 4.4.
Then press Check my work. It calls session_cookie and splits the text itself: the name must be __Host-session, with Secure, HttpOnly, SameSite=Lax, Path=/, no Domain, and a Max-Age from 1 to ABSOLUTE_LIMIT. The order and the case of the attributes do not matter, and a value built with an session_cookie("", max_age=0) must have the same name and flags. Through handle(), with a user it has not shown you, it logs in, asks GET /me with the new cookie and with a cookie named session that holds the same id, which must answer null, and logs out, which must remove the reading_server.handle and reading_server.sample_state: one whose cookie has every flag, where your test must pass; one whose cookie has no Secure, where it must fail; and one that refuses every login, where it must fail. Last, it reads row F4 of SECURITY.md for the name of your test and a status that starts with closed. Nothing from your