Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES/13168.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Enforced the ``__Secure-`` and ``__Host-`` cookie name prefixes
(RFC 6265bis, section 4.1.3) in :class:`~aiohttp.CookieJar`, so a
prefixed cookie that is not host-locked and secure is no longer stored
-- by :user:`arshsmith1`.
24 changes: 24 additions & 0 deletions aiohttp/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,21 @@ def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> No
tmp[name] = cookie # type: ignore[assignment]
cookie = tmp[name]

# RFC 6265bis 4.1.3: enforce the __Secure- and __Host- name
# prefixes so a related-origin (sibling subdomain) or on-path party
# cannot forge a cookie the target server trusts as host-locked and
# secure. Only applied to cookies from an actual response; a jar
# seeded programmatically has no response host to check against.
if hostname is not None:
lower_name = name.lower()
if lower_name.startswith(("__secure-", "__host-")):
if not cookie["secure"] or not self._is_secure_origin(response_url):
continue
if lower_name.startswith("__host-") and (
cookie["domain"] or cookie["path"] != "/"
):
continue

domain = cookie["domain"]

# ignore domains with trailing dots
Expand Down Expand Up @@ -495,6 +510,15 @@ def _build_morsel(self, cookie: Morsel[str]) -> Morsel[str]:
morsel.__setstate__({"key": cookie.key, "value": value, "coded_value": coded_value}) # type: ignore[attr-defined]
return morsel

def _is_secure_origin(self, url: URL) -> bool:
"""Whether cookies from url should be treated as a secure origin."""
if url.scheme in ("https", "wss"):
return True
if self._treat_as_secure_origin:
with contextlib.suppress(ValueError):
return url.origin() in self._treat_as_secure_origin
return False

@staticmethod
def _is_domain_match(domain: str, hostname: str) -> bool:
"""Implements domain matching adhering to RFC 6265."""
Expand Down
59 changes: 59 additions & 0 deletions tests/test_cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,65 @@ async def test_ignore_domain_ending_with_dot() -> None:
assert cookies_sent.output(header="Cookie:") == ""


@pytest.mark.parametrize(
("set_cookie", "url"),
[
# __Host- with a Domain attribute is not host-only.
(
"__Host-sid=x; Domain=example.com; Path=/; Secure",
"https://sub.example.com/",
),
# __Host- without Path=/.
("__Host-sid=x; Path=/admin; Secure", "https://example.com/admin"),
# __Host- without Secure.
("__Host-sid=x; Path=/", "https://example.com/"),
# Prefixes must not be accepted over an insecure origin.
("__Host-sid=x; Path=/; Secure", "http://example.com/"),
("__Secure-sid=x; Secure", "http://example.com/"),
# __Secure- without the Secure attribute.
("__Secure-sid=x", "https://example.com/"),
# Prefix match is case-insensitive, so casing cannot bypass it.
(
"__host-sid=x; Domain=example.com; Path=/; Secure",
"https://sub.example.com/",
),
],
)
async def test_cookie_prefix_rejected(set_cookie: str, url: str) -> None:
jar = CookieJar()
jar.update_cookies(SimpleCookie(set_cookie), URL(url))
assert len(jar) == 0


@pytest.mark.parametrize(
("set_cookie", "url"),
[
("__Host-sid=x; Path=/; Secure", "https://example.com/"),
("__Secure-sid=x; Secure", "https://example.com/"),
("__Secure-sid=x; Domain=example.com; Secure", "https://www.example.com/"),
],
)
async def test_cookie_prefix_accepted(set_cookie: str, url: str) -> None:
jar = CookieJar()
jar.update_cookies(SimpleCookie(set_cookie), URL(url))
assert len(jar) == 1


async def test_cookie_prefix_programmatic_seed_unaffected() -> None:
# Seeding the jar without a response URL is a trusted path and keeps working.
jar = CookieJar()
jar.update_cookies(SimpleCookie("__Host-seed=x; Path=/; Secure"))
assert "__Host-seed" in jar.filter_cookies(URL("https://any.example/"))


async def test_cookie_prefix_treat_as_secure_origin() -> None:
jar = CookieJar(treat_as_secure_origin=URL("http://example.com"))
jar.update_cookies(
SimpleCookie("__Host-sid=x; Path=/; Secure"), URL("http://example.com/")
)
assert "__Host-sid" in jar.filter_cookies(URL("http://example.com/"))


class TestCookieJarSafe:
@pytest.fixture(autouse=True)
def setup_cookies(
Expand Down
Loading