Keep every cookie of a domain when importing a cookies.txt (#3031)

This commit is contained in:
Ashvin
2026-08-28 13:20:00 +02:00
committed by GitHub
parent a7a87f511a
commit 3ffffd7225
2 changed files with 80 additions and 7 deletions
+12 -7
View File
@@ -191,13 +191,18 @@ def import_aiohttp_cookies(cookiestxt_filename):
cookies = CookieJar()
cookies_list = []
for domain in cookies_obj._cookies.values(): # type: ignore[attr-defined]
for key, cookie in next(iter(domain.values())).items():
c: Morsel = Morsel()
c.set(key, cookie.value, cookie.value)
c["domain"] = cookie.domain
c["path"] = cookie.path
cookies_list.append((key, c))
# Iterate the jar itself rather than its internal {domain: {path: {name}}}
# mapping: picking a single path bucket per domain silently dropped every
# cookie stored under the domain's other paths.
for cookie in cookies_obj:
c: Morsel = Morsel()
# A valueless cookie ("name" with no "=value") parses as value None,
# which used to reach the wire as the literal string "None".
value = cookie.value or ""
c.set(cookie.name, value, value)
c["domain"] = cookie.domain
c["path"] = cookie.path
cookies_list.append((cookie.name, c))
cookies.update_cookies(cookies_list)
+68
View File
@@ -69,6 +69,74 @@ async def test_import_aiohttp_cookies(cookie_test_server):
assert result == {'cookies': {'a': 'b'}}
# A real browser export usually stores several cookies per domain under
# different paths. COOKIES_TXT above happens to keep everything on "/", which
# is why the path handling below went untested for so long.
MULTIPATH_COOKIES_TXT = """# Netscape HTTP Cookie File
.example.com TRUE / FALSE 2147483647 sessionid SESSION
.example.com TRUE /account FALSE 2147483647 csrftoken CSRF
.example.com TRUE /api FALSE 2147483647 authtoken AUTH
"""
# A cookie stored with no value at all: the name column is empty and the value
# column carries the name, which http.cookiejar parses as value None.
VALUELESS_COOKIE_TXT = """# Netscape HTTP Cookie File
.example.com TRUE / FALSE 2147483647 consent
"""
def _write(tmp_path, content):
cookies_file = tmp_path / "cookies.txt"
cookies_file.write_text(content, encoding="utf-8")
return str(cookies_file)
@pytest.mark.asyncio
async def test_import_aiohttp_cookies_keeps_every_path_of_a_domain(tmp_path):
"""All of a domain's cookies must survive the import, not just one path.
MozillaCookieJar stores cookies as {domain: {path: {name: cookie}}}. Taking
a single path bucket per domain silently discarded every cookie saved under
the domain's other paths, so an authenticated scan went out missing its CSRF
and auth tokens and simply looked logged out.
"""
cookie_jar = import_aiohttp_cookies(_write(tmp_path, MULTIPATH_COOKIES_TXT))
imported = {morsel.key for morsel in cookie_jar}
assert imported == {"sessionid", "csrftoken", "authtoken"}
@pytest.mark.asyncio
async def test_import_aiohttp_cookies_offers_each_cookie_on_its_own_path(tmp_path):
"""Path scoping still has to hold once every cookie is imported."""
cookie_jar = import_aiohttp_cookies(_write(tmp_path, MULTIPATH_COOKIES_TXT))
def sent_to(url):
return {
key: morsel.value
for key, morsel in cookie_jar.filter_cookies(yarl.URL(url)).items()
}
assert sent_to("http://www.example.com/") == {"sessionid": "SESSION"}
assert sent_to("http://www.example.com/account/profile") == {
"sessionid": "SESSION",
"csrftoken": "CSRF",
}
assert sent_to("http://www.example.com/api/v1/users") == {
"sessionid": "SESSION",
"authtoken": "AUTH",
}
@pytest.mark.asyncio
async def test_import_aiohttp_cookies_does_not_invent_a_none_value(tmp_path):
"""A valueless cookie must not reach the wire as the literal text 'None'."""
cookie_jar = import_aiohttp_cookies(_write(tmp_path, VALUELESS_COOKIE_TXT))
sent = cookie_jar.filter_cookies(yarl.URL("http://www.example.com/"))
assert sent["consent"].value == ""
# ---- OnlyFans signing tests (pure-compute, no network) ----