Skip to content

Commit a6687c2

Browse files
authored
Remove pickle loading from CookieJar (#12111)
1 parent 3f30561 commit a6687c2

5 files changed

Lines changed: 9 additions & 204 deletions

File tree

CHANGES/12091.bugfix.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

CHANGES/12111.breaking.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Removed legacy pickle loading from :py:meth:`~aiohttp.CookieJar.load`.
2+
:py:meth:`~aiohttp.CookieJar.load` now exclusively uses JSON format
3+
-- by :user:`YuvalElbar6`.

aiohttp/cookiejar.py

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import heapq
55
import itertools
66
import json
7-
import os # noqa
87
import pathlib
9-
import pickle
108
import re
119
import time
1210
import warnings
@@ -38,41 +36,6 @@
3836
_SIMPLE_COOKIE = SimpleCookie()
3937

4038

41-
class _RestrictedCookieUnpickler(pickle.Unpickler):
42-
"""A restricted unpickler that only allows cookie-related types.
43-
44-
This prevents arbitrary code execution when loading pickled cookie data
45-
from untrusted sources. Only types that are expected in a serialized
46-
CookieJar are permitted.
47-
48-
See: https://docs.python.org/3/library/pickle.html#restricting-globals
49-
"""
50-
51-
_ALLOWED_CLASSES: frozenset[tuple[str, str]] = frozenset(
52-
{
53-
# Core cookie types
54-
("http.cookies", "SimpleCookie"),
55-
("http.cookies", "Morsel"),
56-
# Container types used by CookieJar._cookies
57-
("collections", "defaultdict"),
58-
# builtins that pickle uses for reconstruction
59-
("builtins", "tuple"),
60-
("builtins", "set"),
61-
("builtins", "frozenset"),
62-
("builtins", "dict"),
63-
}
64-
)
65-
66-
def find_class(self, module: str, name: str) -> type:
67-
if (module, name) not in self._ALLOWED_CLASSES:
68-
raise pickle.UnpicklingError(
69-
f"Forbidden class: {module}.{name}. "
70-
"CookieJar.load() only allows cookie-related types for security. "
71-
"See https://docs.python.org/3/library/pickle.html#restricting-globals"
72-
)
73-
return super().find_class(module, name) # type: ignore[no-any-return]
74-
75-
7639
class CookieJar(AbstractCookieJar):
7740
"""Implements cookie storage adhering to RFC 6265."""
7841

@@ -174,25 +137,15 @@ def save(self, file_path: PathLike) -> None:
174137
json.dump(data, f, indent=2)
175138

176139
def load(self, file_path: PathLike) -> None:
177-
"""Load cookies from a file.
178-
179-
Tries to load JSON format first. Falls back to loading legacy
180-
pickle format (using a restricted unpickler) for backward
181-
compatibility with existing cookie files.
140+
"""Load cookies from a JSON file.
182141
183142
:param file_path: Path to file from where cookies will be
184143
imported, :class:`str` or :class:`pathlib.Path` instance.
185144
"""
186145
file_path = pathlib.Path(file_path)
187-
# Try JSON format first
188-
try:
189-
with file_path.open(mode="r", encoding="utf-8") as f:
190-
data = json.load(f)
191-
self._cookies = self._load_json_data(data)
192-
except (json.JSONDecodeError, UnicodeDecodeError, ValueError):
193-
# Fall back to legacy pickle format with restricted unpickler
194-
with file_path.open(mode="rb") as f:
195-
self._cookies = _RestrictedCookieUnpickler(f).load()
146+
with file_path.open(mode="r", encoding="utf-8") as f:
147+
data = json.load(f)
148+
self._cookies = self._load_json_data(data)
196149

197150
def _load_json_data(
198151
self, data: dict[str, dict[str, dict[str, str | bool]]]
@@ -495,8 +448,7 @@ def _build_morsel(self, cookie: Morsel[str]) -> Morsel[str]:
495448
coded_value = value = cookie.value
496449
# We use __setstate__ instead of the public set() API because it allows us to
497450
# bypass validation and set already validated state. This is more stable than
498-
# setting protected attributes directly and unlikely to change since it would
499-
# break pickling.
451+
# setting protected attributes directly.
500452
morsel.__setstate__({"key": cookie.key, "value": value, "coded_value": coded_value}) # type: ignore[attr-defined]
501453
return morsel
502454

docs/client_reference.rst

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,25 +2498,12 @@ Utilities
24982498
Write a JSON representation of cookies into the file
24992499
at provided path.
25002500

2501-
.. versionchanged:: 3.14
2502-
2503-
Previously used pickle format. Now uses JSON for safe
2504-
serialization.
2505-
25062501
:param file_path: Path to file where cookies will be serialized,
25072502
:class:`str` or :class:`pathlib.Path` instance.
25082503

25092504
.. method:: load(file_path)
25102505

2511-
Load cookies from the file at provided path. Tries JSON format
2512-
first, then falls back to legacy pickle format (using a restricted
2513-
unpickler that only allows cookie-related types) for backward
2514-
compatibility with existing cookie files.
2515-
2516-
.. versionchanged:: 3.14
2517-
2518-
Now loads JSON format by default. Falls back to restricted
2519-
pickle for files saved by older versions.
2506+
Load cookies from a JSON file at the provided path.
25202507

25212508
:param file_path: Path to file from where cookies will be
25222509
imported, :class:`str` or :class:`pathlib.Path` instance.

tests/test_cookiejar.py

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import heapq
33
import itertools
44
import logging
5-
import pickle
6-
import sys
75
from http.cookies import BaseCookie, Morsel, SimpleCookie
86
from operator import not_
97
from pathlib import Path
@@ -17,13 +15,6 @@
1715
from aiohttp.typedefs import LooseCookies
1816

1917

20-
def dump_cookiejar() -> bytes: # pragma: no cover
21-
"""Create pickled data for test_pickle_format()."""
22-
cj = CookieJar()
23-
cj.update_cookies(_cookies_to_send())
24-
return pickle.dumps(cj._cookies, pickle.HIGHEST_PROTOCOL)
25-
26-
2718
def _cookies_to_send() -> SimpleCookie:
2819
return SimpleCookie(
2920
"shared-cookie=first; "
@@ -1027,41 +1018,6 @@ async def test_cookie_jar_clear_domain() -> None:
10271018
next(iterator)
10281019

10291020

1030-
def test_pickle_format(cookies_to_send: SimpleCookie) -> None:
1031-
"""Test if cookiejar pickle format breaks.
1032-
1033-
If this test fails, it may indicate that saved cookiejars will stop working.
1034-
If that happens then:
1035-
1. Avoid releasing the change in a bugfix release.
1036-
2. Try to include a migration script in the release notes (example below).
1037-
3. Use dump_cookiejar() at the top of this file to update `pickled`.
1038-
1039-
Depending on the changes made, a migration script might look like:
1040-
import pickle
1041-
with file_path.open("rb") as f:
1042-
cookies = pickle.load(f)
1043-
1044-
morsels = [(name, m) for c in cookies.values() for name, m in c.items()]
1045-
cookies.clear()
1046-
for name, m in morsels:
1047-
cookies[(m["domain"], m["path"])][name] = m
1048-
1049-
with file_path.open("wb") as f:
1050-
pickle.dump(cookies, f, pickle.HIGHEST_PROTOCOL)
1051-
"""
1052-
if sys.version_info < (3, 14):
1053-
pickled = b"\x80\x04\x95\xc8\x0b\x00\x00\x00\x00\x00\x00\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94\x85\x94R\x94(\x8c\x00\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rshared-cookie\x94h\x03\x8c\x06Morsel\x94\x93\x94)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\x01/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x08\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(\x8c\x03key\x94h\x0b\x8c\x05value\x94\x8c\x05first\x94\x8c\x0bcoded_value\x94h\x1cubs\x8c\x0bexample.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\rdomain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x1e\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah!h\x1b\x8c\x06second\x94h\x1dh-ub\x8c\x14dotted-domain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0bexample.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah.h\x1b\x8c\x05fifth\x94h\x1dh;ubu\x8c\x11test1.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain1-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h<\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah?h\x1b\x8c\x05third\x94h\x1dhKubs\x8c\x11test2.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain2-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94hL\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ahOh\x1b\x8c\x06fourth\x94h\x1dh[ubs\x8c\rdifferent.org\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x17different-domain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\\\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah_h\x1b\x8c\x05sixth\x94h\x1dhkubs\x8c\nsecure.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rsecure-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94hl\x8c\x07max-age\x94h\x08\x8c\x06secure\x94\x88\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ahoh\x1b\x8c\x07seventh\x94h\x1dh{ubs\x8c\x0cpathtest.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x0eno-path-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h|\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x7fh\x1b\x8c\x06eighth\x94h\x1dh\x8bub\x8c\x0cpath1-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0cpathtest.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x8ch\x1b\x8c\x05ninth\x94h\x1dh\x99ubu\x8c\x0cpathtest.com\x94\x8c\x04/one\x94\x86\x94h\x05)\x81\x94\x8c\x0cpath2-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x9b\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x9a\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x9eh\x1b\x8c\x05tenth\x94h\x1dh\xaaubs\x8c\x0cpathtest.com\x94\x8c\x08/one/two\x94\x86\x94h\x05)\x81\x94(\x8c\x0cpath3-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\xac\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xab\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xafh\x1b\x8c\x08eleventh\x94h\x1dh\xbbub\x8c\x0cpath4-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\t/one/two/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0cpathtest.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xbch\x1b\x8c\x07twelfth\x94h\x1dh\xcaubu\x8c\x0fexpirestest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0eexpires-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94\x8c\x1cTue, 1 Jan 2999 12:00:00 GMT\x94\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xcb\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xceh\x1b\x8c\nthirteenth\x94h\x1dh\xdbubs\x8c\x0emaxagetest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0emax-age-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xdc\x8c\x07max-age\x94\x8c\x0260\x94\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xdfh\x1b\x8c\nfourteenth\x94h\x1dh\xecubs\x8c\x12invalid-values.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x16invalid-max-age-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xed\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xf0h\x1b\x8c\tfifteenth\x94h\x1dh\xfcub\x8c\x16invalid-expires-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x12invalid-values.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xfdh\x1b\x8c\tsixteenth\x94h\x1dj\n\x01\x00\x00ubuu."
1054-
else:
1055-
pickled = b'\x80\x05\x95\x06\x08\x00\x00\x00\x00\x00\x00\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94\x85\x94R\x94(\x8c\x00\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rshared-cookie\x94h\x03\x8c\x06Morsel\x94\x93\x94)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\x01/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x08\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08\x8c\x0bpartitioned\x94h\x08u}\x94(\x8c\x03key\x94h\x0b\x8c\x05value\x94\x8c\x05first\x94\x8c\x0bcoded_value\x94h\x1dubs\x8c\x0bexample.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\rdomain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h\x1fh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh"h\x1c\x8c\x06second\x94h\x1eh%ub\x8c\x14dotted-domain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x0bexample.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh&h\x1c\x8c\x05fifth\x94h\x1eh*ubu\x8c\x11test1.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain1-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h+h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh.h\x1c\x8c\x05third\x94h\x1eh1ubs\x8c\x11test2.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain2-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h2h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh5h\x1c\x8c\x06fourth\x94h\x1eh8ubs\x8c\rdifferent.org\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x17different-domain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h9h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh<h\x1c\x8c\x05sixth\x94h\x1eh?ubs\x8c\nsecure.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rsecure-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h@h\x14h\x08h\x15\x88h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhCh\x1c\x8c\x07seventh\x94h\x1ehFubs\x8c\x0cpathtest.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x0eno-path-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hGh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhJh\x1c\x8c\x06eighth\x94h\x1ehMub\x8c\x0cpath1-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x0cpathtest.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhNh\x1c\x8c\x05ninth\x94h\x1ehRubu\x8c\x0cpathtest.com\x94\x8c\x04/one\x94\x86\x94h\x05)\x81\x94\x8c\x0cpath2-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10hTh\x12h\x08h\x13hSh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhWh\x1c\x8c\x05tenth\x94h\x1ehZubs\x8c\x0cpathtest.com\x94\x8c\x08/one/two\x94\x86\x94h\x05)\x81\x94(\x8c\x0cpath3-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\\h\x12h\x08h\x13h[h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh_h\x1c\x8c\x08eleventh\x94h\x1ehbub\x8c\x0cpath4-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10\x8c\t/one/two/\x94h\x12h\x08h\x13\x8c\x0cpathtest.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhch\x1c\x8c\x07twelfth\x94h\x1ehhubu\x8c\x0fexpirestest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0eexpires-cookie\x94h\r)\x81\x94(h\x0f\x8c\x1cTue, 1 Jan 2999 12:00:00 GMT\x94h\x10h\x11h\x12h\x08h\x13hih\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhlh\x1c\x8c\nthirteenth\x94h\x1ehpubs\x8c\x0emaxagetest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0emax-age-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hqh\x14\x8c\x0260\x94h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhth\x1c\x8c\nfourteenth\x94h\x1ehxubs\x8c\x12invalid-values.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x16invalid-max-age-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hyh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh|h\x1c\x8c\tfifteenth\x94h\x1eh\x7fub\x8c\x16invalid-expires-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x12invalid-values.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh\x80h\x1c\x8c\tsixteenth\x94h\x1eh\x84ubuu.'
1056-
1057-
cookies = pickle.loads(pickled)
1058-
1059-
cj = CookieJar()
1060-
cj.update_cookies(cookies_to_send)
1061-
1062-
assert cookies == cj._cookies
1063-
1064-
10651021
@pytest.mark.parametrize(
10661022
"url",
10671023
[
@@ -1566,93 +1522,6 @@ async def test_shared_cookie_with_multiple_domains() -> None:
15661522
assert "universal" in jar._morsel_cache[("", "")]
15671523

15681524

1569-
# === Security tests for restricted unpickler and JSON save/load ===
1570-
1571-
1572-
def test_load_rejects_malicious_pickle(tmp_path: Path) -> None:
1573-
"""Verify CookieJar.load() blocks arbitrary code execution via pickle.
1574-
1575-
A crafted pickle payload using os.system (or any non-cookie class)
1576-
must be rejected by the restricted unpickler.
1577-
"""
1578-
import os
1579-
1580-
file_path = tmp_path / "malicious.pkl"
1581-
1582-
class RCEPayload:
1583-
def __reduce__(self) -> tuple[object, ...]:
1584-
return (os.system, ("echo PWNED",))
1585-
1586-
with open(file_path, "wb") as f:
1587-
pickle.dump(RCEPayload(), f, pickle.HIGHEST_PROTOCOL)
1588-
1589-
jar = CookieJar()
1590-
with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
1591-
jar.load(file_path)
1592-
1593-
1594-
def test_load_rejects_eval_payload(tmp_path: Path) -> None:
1595-
"""Verify CookieJar.load() blocks eval-based pickle payloads."""
1596-
file_path = tmp_path / "eval_payload.pkl"
1597-
1598-
class EvalPayload:
1599-
def __reduce__(self) -> tuple[object, ...]:
1600-
return (eval, ("__import__('os').system('echo PWNED')",))
1601-
1602-
with open(file_path, "wb") as f:
1603-
pickle.dump(EvalPayload(), f, pickle.HIGHEST_PROTOCOL)
1604-
1605-
jar = CookieJar()
1606-
with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
1607-
jar.load(file_path)
1608-
1609-
1610-
def test_load_rejects_subprocess_payload(tmp_path: Path) -> None:
1611-
"""Verify CookieJar.load() blocks subprocess-based pickle payloads."""
1612-
import subprocess
1613-
1614-
file_path = tmp_path / "subprocess_payload.pkl"
1615-
1616-
class SubprocessPayload:
1617-
def __reduce__(self) -> tuple[object, ...]:
1618-
return (subprocess.call, (["echo", "PWNED"],))
1619-
1620-
with open(file_path, "wb") as f:
1621-
pickle.dump(SubprocessPayload(), f, pickle.HIGHEST_PROTOCOL)
1622-
1623-
jar = CookieJar()
1624-
with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
1625-
jar.load(file_path)
1626-
1627-
1628-
def test_load_falls_back_to_pickle(
1629-
tmp_path: Path,
1630-
cookies_to_receive: SimpleCookie,
1631-
) -> None:
1632-
"""Verify load() falls back to restricted pickle for legacy cookie files.
1633-
1634-
Existing cookie files saved with older versions of aiohttp used pickle.
1635-
load() should detect that the file is not JSON and fall back to the
1636-
restricted pickle unpickler for backward compatibility.
1637-
"""
1638-
file_path = tmp_path / "legit.pkl"
1639-
1640-
# Write a legacy pickle file directly (as old aiohttp save() would)
1641-
jar_save = CookieJar()
1642-
jar_save.update_cookies(cookies_to_receive)
1643-
with file_path.open(mode="wb") as f:
1644-
pickle.dump(jar_save._cookies, f, pickle.HIGHEST_PROTOCOL)
1645-
1646-
jar_load = CookieJar()
1647-
jar_load.load(file_path=file_path)
1648-
1649-
jar_test = SimpleCookie()
1650-
for cookie in jar_load:
1651-
jar_test[cookie.key] = cookie
1652-
1653-
assert jar_test == cookies_to_receive
1654-
1655-
16561525
def test_save_load_json_roundtrip(
16571526
tmp_path: Path,
16581527
cookies_to_receive: SimpleCookie,

0 commit comments

Comments
 (0)