0%

Origins and Cross-Site Requests · practice

Which Cookies Travel

When Ana logs in, the reply sets the , and a browser puts it in its cookie jar. In the model the jar is a , and a cookie is a . Here is the of Chapter 4 and the dict it becomes, with the shortened. The header is one line, broken here after 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 of the page that started it.

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 , so a request to 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:

  1. the two paths are equal;

  2. the request path starts with the cookie path, and the cookie path ends in /;

  3. 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 without a path, such as 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. with Python left 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 from Lesson 2. If it does, every cookie may go. If it does not, the attribute decides:

  • Strict: the cookie stays home.

  • Lax: the cookie goes only with a top-level navigation whose is 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, Lax lets 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:

SameSiteThe requestGoes along?
Strictinside the siteyes
Strictcross-site linkno
Strictcross-site form postno
Laxinside the siteyes
Laxcross-site linkyes
Laxcross-site form postno
Laxcross-site image?
Nonecross-site form post?
Nonecross-site imageyes
(missing)cross-site linkyes
(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 Secure cookie set by a response from an http URL;

  • a cookie with SameSite=None and no Secure;

  • a cookie whose name starts with __Host- that is not Secure, 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: from steps.OTHER, the result is [].

  • test_lax_cookie_travels_within_the_site: from steps.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 as Lax, 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, an the draft allows. Firefox and Safari treat a missing SameSite as None.

  • SameSite=None without Secure. Chromium and Firefox refuse to store it, as the model does. Safari stores it.

  • The scheme in a site. Chromium counts http://reading.example and https://reading.example as two sites, as the model does. Firefox and Safari count them as one when they apply SameSite.

  • localhost. Chrome and Firefox accept a Secure cookie from http://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 Domain attribute, 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 a request carries, in mini_browser.py, and two tests of them.

A cookie is a with the keys name, value, host, path, secure (True or False), and samesite ("Strict", "Lax", "None", or "" when the cookie names none). A jar is a of such dicts.

  • path_matches(cookie_path, request_path) returns True when 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 returns False.

  • samesite_allows(samesite, *, same_site_request, method, top_level) returns True for "None", and for every when same_site_request is true. Across sites, "Strict" gets False, and "Lax" and "" get True only when top_level is true and method is in LAX_METHODS.

  • may_store(cookie, url) returns False for a Secure cookie when url is not an https , for SameSite=None without Secure, and for a name that starts with __Host- when the cookie is not Secure or its path is not /. Otherwise it returns True.

  • cookies_to_send(jar, url, page_origin, *, method, top_level) returns a new list with the cookies of jar that go along with the request, in jar order. A cookie goes when its host equals the host of url, its path matches the path of url (an empty path counts as /), it is not Secure or url is an https URL, and samesite_allows says yes for same_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 on steps.OTHER, the result is [].

  • test_lax_cookie_travels_within_the_site: started by a page on steps.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 with values you have not seen. 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 , to another host of the site, and from pages inside the site, on another site, and on the 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 . Nothing from your is graded.