Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
83315ef
Bump version (#11685)
Dreamsorcerer Oct 17, 2025
381334b
[PR #11686/42fc48a6 backport][3.14] Raise benchmark timeout to 12 min…
patchback[bot] Oct 18, 2025
e6042d2
[PR #11686/42fc48a6 backport][3.13] Raise benchmark timeout to 12 min…
patchback[bot] Oct 18, 2025
f401e99
Bump iniconfig from 2.1.0 to 2.3.0 (#11692)
dependabot[bot] Oct 20, 2025
9529279
Bump pydantic from 2.12.2 to 2.12.3 (#11693)
dependabot[bot] Oct 20, 2025
0354bf1
Bump cython from 3.1.4 to 3.1.5 (#11694)
dependabot[bot] Oct 20, 2025
d7c3e1f
Bump sigstore/gh-action-sigstore-python from 3.0.1 to 3.1.0 (#11699)
dependabot[bot] Oct 21, 2025
231a172
Bump regex from 2025.9.18 to 2025.10.23 (#11702)
dependabot[bot] Oct 22, 2025
442d38f
Bump cython from 3.1.5 to 3.1.6 (#11710)
dependabot[bot] Oct 24, 2025
1d6513a
Bump pytest-codspeed from 4.1.1 to 4.2.0 (#11711)
dependabot[bot] Oct 24, 2025
322d177
Bump python-on-whales from 0.78.0 to 0.79.0 (#11712)
dependabot[bot] Oct 24, 2025
b4d9b89
Bump actions/download-artifact from 5 to 6 (#11721)
dependabot[bot] Oct 27, 2025
6d22d3a
Bump pip from 25.2 to 25.3 (#11722)
dependabot[bot] Oct 27, 2025
57ad7fa
[PR #11714/0d77d0d6 backport][3.14] Fix loading netrc when NETRC env …
patchback[bot] Oct 28, 2025
b734e04
[PR #11714/0d77d0d6 backport][3.13] Fix loading netrc when NETRC env …
patchback[bot] Oct 28, 2025
990c6b4
[PR #11724/82ce525b backport][3.14] Ensure cookies are still parsed a…
bdraco Oct 28, 2025
95daf0c
[PR #11724/82ce525b backport][3.13] Ensure cookies are still parsed a…
bdraco Oct 28, 2025
baf646f
[PR #11726/6cffcfd backport][3.14] Fix WebSocket compressed sends to …
bdraco Oct 28, 2025
5c75e63
[PR #11726/6cffcfd backport][3.13] Fix WebSocket compressed sends to …
bdraco Oct 28, 2025
91547df
Release 3.13.2 (#11733)
bdraco Oct 28, 2025
e1aec0a
Move dependency metadata from `setup.cfg` to `pyproject.toml`
cdce8p Oct 28, 2025
9b8f4b9
Merge branch '3.13' into 3.14
bdraco Oct 28, 2025
861c621
Merge branch '3.14'
bdraco Oct 28, 2025
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
Prev Previous commit
Next Next commit
[PR aio-libs#11724/82ce525b backport][3.14] Ensure cookies are still …
…parsed after a malformed cookie (aio-libs#11730)
  • Loading branch information
bdraco authored Oct 28, 2025
commit 990c6b4ceb327d981fe766f039340f7e1c526522
1 change: 1 addition & 0 deletions CHANGES/11632.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed cookie parser to continue parsing subsequent cookies when encountering a malformed cookie that fails regex validation, such as Google's ``g_state`` cookie with unescaped quotes -- by :user:`bdraco`.
35 changes: 32 additions & 3 deletions aiohttp/_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]:
attribute names (like 'path' or 'secure') should be treated as cookies.

This parser uses the same regex-based approach as parse_set_cookie_headers
to properly handle quoted values that may contain semicolons.
to properly handle quoted values that may contain semicolons. When the
regex fails to match a malformed cookie, it falls back to simple parsing
to ensure subsequent cookies are not lost
https://github.com/aio-libs/aiohttp/issues/11632

Args:
header: The Cookie header value to parse
Expand All @@ -178,14 +181,40 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]:
return []

cookies: list[tuple[str, Morsel[str]]] = []
morsel: Morsel[str]
i = 0
n = len(header)

while i < n:
# Use the same pattern as parse_set_cookie_headers to find cookies
match = _COOKIE_PATTERN.match(header, i)
if not match:
break
# Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632
# Find next semicolon to skip or attempt simple key=value parsing
next_semi = header.find(";", i)
eq_pos = header.find("=", i)

# Try to extract key=value if '=' comes before ';'
if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi):
end_pos = next_semi if next_semi != -1 else n
key = header[i:eq_pos].strip()
value = header[eq_pos + 1 : end_pos].strip()

# Validate the name (same as regex path)
if not _COOKIE_NAME_RE.match(key):
internal_logger.warning(
"Can not load cookie: Illegal cookie name %r", key
)
else:
morsel = Morsel()
morsel.__setstate__( # type: ignore[attr-defined]
{"key": key, "value": _unquote(value), "coded_value": value}
)
cookies.append((key, morsel))

# Move to next cookie or end
i = next_semi + 1 if next_semi != -1 else n
continue

key = match.group("key")
value = match.group("val") or ""
Expand All @@ -197,7 +226,7 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]:
continue

# Create new morsel
morsel: Morsel[str] = Morsel()
morsel = Morsel()
# Preserve the original value as coded_value (with quotes if present)
# We use __setstate__ instead of the public set() API because it allows us to
# bypass validation and set already validated state. This is more stable than
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ un
unawaited
unclosed
undercounting
unescaped
unhandled
unicode
unittest
Expand Down
137 changes: 136 additions & 1 deletion tests/test_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,6 @@ def test_parse_cookie_header_empty() -> None:
assert parse_cookie_header(" ") == []


@pytest.mark.xfail(reason="https://github.com/aio-libs/aiohttp/issues/11632")
def test_parse_cookie_gstate_header() -> None:
header = (
"_ga=ga; "
Expand Down Expand Up @@ -1444,6 +1443,142 @@ def test_parse_cookie_header_illegal_names(caplog: pytest.LogCaptureFixture) ->
assert "Can not load cookie: Illegal cookie name 'invalid,cookie'" in caplog.text


def test_parse_cookie_header_large_value() -> None:
"""Test that large cookie values don't cause DoS."""
large_value = "A" * 8192
header = f"normal=value; large={large_value}; after=cookie"

result = parse_cookie_header(header)
cookie_names = [name for name, _ in result]

assert len(result) == 3
assert "normal" in cookie_names
assert "large" in cookie_names
assert "after" in cookie_names

large_cookie = next(morsel for name, morsel in result if name == "large")
assert len(large_cookie.value) == 8192


def test_parse_cookie_header_multiple_equals() -> None:
"""Test handling of multiple equals signs in cookie values."""
header = "session=abc123; data=key1=val1&key2=val2; token=xyz"

result = parse_cookie_header(header)

assert len(result) == 3

name1, morsel1 = result[0]
assert name1 == "session"
assert morsel1.value == "abc123"

name2, morsel2 = result[1]
assert name2 == "data"
assert morsel2.value == "key1=val1&key2=val2"

name3, morsel3 = result[2]
assert name3 == "token"
assert morsel3.value == "xyz"


def test_parse_cookie_header_fallback_preserves_subsequent_cookies() -> None:
"""Test that fallback parser doesn't lose subsequent cookies."""
header = 'normal=value; malformed={"json":"value"}; after1=cookie1; after2=cookie2'

result = parse_cookie_header(header)
cookie_names = [name for name, _ in result]

assert len(result) == 4
assert cookie_names == ["normal", "malformed", "after1", "after2"]

name1, morsel1 = result[0]
assert morsel1.value == "value"

name2, morsel2 = result[1]
assert morsel2.value == '{"json":"value"}'

name3, morsel3 = result[2]
assert morsel3.value == "cookie1"

name4, morsel4 = result[3]
assert morsel4.value == "cookie2"


def test_parse_cookie_header_whitespace_in_fallback() -> None:
"""Test that fallback parser handles whitespace correctly."""
header = "a=1; b = 2 ; c= 3; d =4"

result = parse_cookie_header(header)

assert len(result) == 4
for name, morsel in result:
assert name in ("a", "b", "c", "d")
assert morsel.value in ("1", "2", "3", "4")


def test_parse_cookie_header_empty_value_in_fallback() -> None:
"""Test that fallback handles empty values correctly."""
header = "normal=value; empty=; another=test"

result = parse_cookie_header(header)

assert len(result) == 3

name1, morsel1 = result[0]
assert name1 == "normal"
assert morsel1.value == "value"

name2, morsel2 = result[1]
assert name2 == "empty"
assert morsel2.value == ""

name3, morsel3 = result[2]
assert name3 == "another"
assert morsel3.value == "test"


def test_parse_cookie_header_invalid_name_in_fallback(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that fallback parser rejects cookies with invalid names."""
header = 'normal=value; invalid,name={"x":"y"}; another=test'

result = parse_cookie_header(header)

assert len(result) == 2

name1, morsel1 = result[0]
assert name1 == "normal"
assert morsel1.value == "value"

name2, morsel2 = result[1]
assert name2 == "another"
assert morsel2.value == "test"

assert "Can not load cookie: Illegal cookie name 'invalid,name'" in caplog.text


def test_parse_cookie_header_empty_key_in_fallback(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that fallback parser logs warning for empty cookie names."""
header = 'normal=value; ={"malformed":"json"}; another=test'

result = parse_cookie_header(header)

assert len(result) == 2

name1, morsel1 = result[0]
assert name1 == "normal"
assert morsel1.value == "value"

name2, morsel2 = result[1]
assert name2 == "another"
assert morsel2.value == "test"

assert "Can not load cookie: Illegal cookie name ''" in caplog.text


@pytest.mark.parametrize(
("input_str", "expected"),
[
Expand Down