Origins and Cross-Site Requests · practice
Which Cookies Travel
When Ana logs in, the reply sets the Max-Age to fit the page:
set-cookie: __Host-session=<22 characters>; HttpOnly; Max-Age=28800;
Path=/; SameSite=Lax; Secure
{
"name": "__Host-session",
"value": "<22 characters>",
"host": "reading.example",
"path": "/",
"secure": True,
"samesite": "Lax",
}
host is the host that set the cookie. samesite is "Strict", "Lax", "None", or "" for a cookie that names none.
For every request a page starts, the browser asks of every cookie in the jar: does this one go along? There are four tests, in this order: host, path, Secure, SameSite. A cookie that fails one stays home. cookies_to_send(jar, url, page_origin, *, method, top_level) is that url is where the request goes, and page_origin is the
Host
The cookie goes only to the exact host that set it: cookie["host"] must equal urlsplit(url).hostname. The session cookie names no Domain, so app.reading.example never receives it.
The test compares hosts, not origins. A cookie is not tied to a https://reading.example:8443 carries the cookies of https://reading.example.
The jar holds the session cookie of reading.example. A request goes to https://reading.example:8443/me from a page of the reading list. Does the cookie go along?
Path
A cookie goes to its own path and to every path below it. path_matches(cookie_path, request_path) is true in three cases:
the two paths are equal;
the request path starts with the cookie path, and the cookie path ends in
/;the request path starts with the cookie path, and its next character is
/.
So Path=/books matches /books/7. It does not match /booksx, which starts with the same letters and is another path. Path=/ matches everything. A https://reading.example, asks for /.
A cookie has Path=/books. Which request path does it match?
Secure
A Secure cookie goes only to an https URL. Secure off, because its server spoke plain HTTP. Chapter 4 turned it on. This test is the rule that makes the flag mean something.
SameSite
First the browser asks whether the request stays inside one site: same_site(page_origin, url), with your
Strict: the cookie stays home.Lax: the cookie goes only with a top-level navigation whoseis GET or HEAD. In the model, a followed link and a submitted form are top-level navigations: the answer becomes the page the reader sees. An image and a request that a script makes are not. A link is a GET, a form can send GET or POST, and no page navigation uses HEAD. So across sites, Laxlets the cookie go along with a followed link or a form sent with GET, and never with a form post, an image, or a script’s request. That is why Lesson 8 insists that no GET changes anything.None: the cookie always goes.No attribute: this model treats the cookie as
Lax, as Chromium does.
samesite_allows(samesite, *, same_site_request, method, top_level) answers for one cookie. The * makes the three names after it keyword-only. The supplied set LAX_METHODS holds "GET" and "HEAD". Four answers in this table are left for you:
SameSite | The request | Goes along? |
|---|---|---|
Strict | inside the site | yes |
Strict | cross-site link | no |
Strict | cross-site form post | no |
Lax | inside the site | yes |
Lax | cross-site link | yes |
Lax | cross-site form post | no |
Lax | cross-site image | ? |
None | cross-site form post | ? |
None | cross-site image | yes |
| (missing) | cross-site link | yes |
| (missing) | cross-site form post | ? |
| (missing) | cross-site image | ? |
A page on https://other.example holds a form that posts to https://reading.example/books/delete. The session cookie is SameSite=Lax. Does it go along?
What a browser refuses to keep
Some cookies never reach the jar. may_store(cookie, url) returns False for three of them:
a
Securecookie set by a response from anhttpURL;a cookie with
SameSite=Noneand noSecure;a cookie whose name starts with
__Host-that is notSecure, or whose path is not/.
Chapter 4 chose the __Host- prefix as a promise. Here is the code that holds a server to it.
A response sets __Host-session=abc; Path=/; HttpOnly with no Secure. What does the model's browser do?
Your task
In mini_browser.py, write the four functions marked for Lesson 3, in this order: path_matches, samesite_allows, may_store, cookies_to_send. The last one walks the jar, applies the four tests with continue, and returns a new list of the cookies that go, in jar order. It never changes the jar.
Fill each question mark by asking your function. This line asks for the first one:
python -c "import mini_browser; print(mini_browser.samesite_allows('Lax', same_site_request=False, method='GET', top_level=False))"
tests/steps.py holds the four page origins as OWN, APP, NOTES, and OTHER. Then write two tests in tests/test_browser.py. Each builds a jar with one cookie dict like the one at the top of this page and calls mini_browser.cookies_to_send for a form post to steps.OWN + "/books/delete", with method="POST" and top_level=True:
test_lax_cookie_stays_home_on_a_cross_site_post: fromsteps.OTHER, the result is[].test_lax_cookie_travels_within_the_site: fromsteps.NOTES, the result equals the jar.
Then run both:
python -m pytest -q tests/test_browser.py
python mini_browser.py
The first ends in 2 passed. In the table of the second, the last column now reads True, True, True, False: of the four pages, only the one on other.example posts its form without the cookie.
Where the model and real browsers differ in this lesson:
A missing
SameSite. The model treats it asLax, as Chromium does and as the current draft of the cookie standard says. Chromium also lets such a cookie go along with a cross-site top-level POST during its first two minutes, anthe draft allows. Firefox and Safari treat a missing SameSiteasNone.SameSite=NonewithoutSecure. Chromium and Firefox refuse to store it, as the model does. Safari stores it.The scheme in a site. Chromium counts
http://reading.exampleandhttps://reading.exampleas two sites, as the model does. Firefox and Safari count them as one when they applySameSite.localhost. Chrome and Firefox accept aSecurecookie fromhttp://localhost, and Safari does not. The model accepts none.The prefix. The draft matches
__Host-without regard to case, and the model matches it exactly.Left out. The
Domainattribute, expiry by time, cookie partitioning, and the blocking of cookies sent to a third party.
Then press Check my work.
Task
Write the rules that decide which mini_browser.py, and two tests of them.
A cookie is a name, value, host, path, secure (True or False), and samesite ("Strict", "Lax", "None", or "" when the cookie names none). A jar is a
path_matches(cookie_path, request_path)returnsTruewhen the paths are equal, or when the request path starts with the cookie path and either the cookie path ends in/or the next character of the request path is/. Otherwise it returnsFalse.samesite_allows(samesite, *, same_site_request, method, top_level)returnsTruefor"None", and for everywhen same_site_requestis true. Across sites,"Strict"getsFalse, and"Lax"and""getTrueonly whentop_levelis true andmethodis inLAX_METHODS.may_store(cookie, url)returnsFalsefor aSecurecookie whenurlis not anhttps, for SameSite=NonewithoutSecure, and for a name that starts with__Host-when the cookie is notSecureor its path is not/. Otherwise it returnsTrue.cookies_to_send(jar, url, page_origin, *, method, top_level)returns a new list with the cookies ofjarthat go along with the request, in jar order. A cookie goes when itshostequals the host ofurl, its path matches the path ofurl(an empty path counts as/), it is notSecureorurlis anhttpsURL, andsamesite_allowssays yes forsame_site(page_origin, url). The jar is not changed.
In tests/test_browser.py, below the Chapter 8 comment, write two tests. Each calls mini_browser.cookies_to_send with a jar that holds one Lax cookie of the host reading.example, for a POST to steps.OWN + "/books/delete" with top_level=True:
test_lax_cookie_stays_home_on_a_cross_site_post: started by a page onsteps.OTHER, the result is[].test_lax_cookie_travels_within_the_site: started by a page onsteps.NOTES, the result equals the jar.
Run python -m pytest -q tests/test_browser.py and python mini_browser.py.
Then press Check my work. It calls the four path_matches gets ten pairs of paths. samesite_allows gets every value of the attribute with requests inside the site and across sites, with GET, HEAD, POST, and DELETE, and with both values of top_level. may_store gets ten cookies, one of them with __Host- in the middle of its name. cookies_to_send gets a jar of eight cookies and fourteen requests: to a deeper path, to a path that only starts with the same letters, to a URL without a path, to an http URL, to another http side of the same host. The jar is compared before and after. Last, it runs your two tests by name, once: each must pass, and each must call mini_browser.cookies_to_send through the