Skip to content

Commit 8a390bc

Browse files
Address PR review: improve tests, Sphinx roles, remove pyupgrade pin
- Use :cve:`2026-3644` and :external+python:exc: roles in changelog - Add pytest IDs to CTL chars from octal parametrize - Parametrize literal CTL char test (was two separate tests) - Use any() instead of list + in for semantic clarity - Replace unittest.mock.patch with monkeypatch fixture (no new dep) - Use @pytest.mark.usefixtures for void fixture injection - Remove pyupgrade language_version: python3.13 pin
1 parent cece051 commit 8a390bc

File tree

3 files changed

+44
-21
lines changed

3 files changed

+44
-21
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ repos:
103103
hooks:
104104
- id: pyupgrade
105105
args: ['--py37-plus']
106-
language_version: python3.13
107106
- repo: https://github.com/PyCQA/flake8
108107
rev: '7.3.0'
109108
hooks:

CHANGES/12395.bugfix.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Fixed a crash (``CookieError``) in the cookie parser when receiving cookies
2-
containing ASCII control characters on CPython builds with the CVE-2026-3644
1+
Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies
2+
containing ASCII control characters on CPython builds with the :cve:`2026-3644`
33
patch. The parser now gracefully falls back to storing the raw, still-escaped
44
``coded_value`` when the decoded value contains control characters, and skips
55
cookies whose raw header contains literal control characters that cannot be

tests/test_cookie_helpers.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
import logging
44
import sys
55
import time
6-
import typing
76
from http.cookies import (
87
CookieError,
98
Morsel,
109
SimpleCookie,
1110
_unquote as simplecookie_unquote,
1211
)
13-
from unittest.mock import patch
1412

1513
import pytest
1614

@@ -1139,8 +1137,18 @@ def test_parse_set_cookie_headers_uses_unquote_with_octal(
11391137
@pytest.mark.parametrize(
11401138
("header", "expected_name", "expected_coded"),
11411139
[
1142-
(r'name="\012newline\012"', "name", r'"\012newline\012"'),
1143-
(r'tab="\011separated\011values"', "tab", r'"\011separated\011values"'),
1140+
pytest.param(
1141+
r'name="\012newline\012"',
1142+
"name",
1143+
r'"\012newline\012"',
1144+
id="newline-octal-012",
1145+
),
1146+
pytest.param(
1147+
r'tab="\011separated\011values"',
1148+
"tab",
1149+
r'"\011separated\011values"',
1150+
id="tab-octal-011",
1151+
),
11441152
],
11451153
)
11461154
def test_parse_set_cookie_headers_ctl_chars_from_octal(
@@ -1164,27 +1172,43 @@ def test_parse_set_cookie_headers_ctl_chars_from_octal(
11641172
# We just ensure it doesn't crash and the coded_value is preserved.
11651173

11661174

1167-
def test_parse_set_cookie_headers_literal_ctl_chars() -> None:
1175+
@pytest.mark.parametrize(
1176+
("header", "expected_name"),
1177+
[
1178+
pytest.param(
1179+
'name="a\x07b"',
1180+
"name",
1181+
id="bel-in-value",
1182+
),
1183+
pytest.param(
1184+
'bad="a\x07b"; good=value',
1185+
"bad",
1186+
id="bel-with-attribute",
1187+
),
1188+
],
1189+
)
1190+
def test_parse_set_cookie_headers_literal_ctl_chars(
1191+
header: str, expected_name: str
1192+
) -> None:
11681193
r"""Ensure literal control characters in a cookie value don't crash the parser.
11691194
11701195
If the raw header itself contains a control character (e.g. BEL \\x07),
11711196
both the decoded value and coded_value are unsalvageable. The parser
11721197
should gracefully skip the cookie instead of raising CookieError.
11731198
"""
1174-
result = parse_set_cookie_headers(['name="a\x07b"'])
1199+
result = parse_set_cookie_headers([header])
11751200
# On CPython with CVE-2026-3644 patch the cookie is skipped;
11761201
# on older builds it may be accepted. Either way, no crash.
11771202
if result:
1178-
assert result[0][0] == "name"
1203+
assert result[0][0] == expected_name
11791204

11801205

11811206
def test_parse_set_cookie_headers_literal_ctl_chars_preserves_others() -> None:
11821207
"""Ensure a cookie with literal control chars doesn't break subsequent cookies."""
11831208
result = parse_set_cookie_headers(['bad="a\x07b"; good=value', "another=cookie"])
11841209
# "good" is an attribute of "bad" (same header), so it's not a separate cookie.
11851210
# "another" is in a separate header and must always be preserved.
1186-
names = [name for name, _ in result]
1187-
assert "another" in names
1211+
assert any(name == "another" for name, _ in result)
11881212

11891213

11901214
# Tests for parse_cookie_header (RFC 6265 compliant Cookie header parser)
@@ -1660,8 +1684,7 @@ def test_parse_cookie_header_literal_ctl_chars() -> None:
16601684
result = parse_cookie_header('name="a\x07b"; good=cookie')
16611685
# On CPython with CVE-2026-3644 patch the bad cookie is skipped;
16621686
# on older builds it may be accepted. Either way, no crash.
1663-
names = [name for name, _ in result]
1664-
assert "good" in names
1687+
assert any(name == "good" for name, _ in result)
16651688

16661689

16671690
@pytest.mark.parametrize(
@@ -1855,23 +1878,24 @@ def test_unquote_compatibility_with_simplecookie(test_value: str) -> None:
18551878

18561879

18571880
@pytest.fixture
1858-
def mock_strict_morsel() -> typing.Iterator[None]:
1881+
def mock_strict_morsel(
1882+
monkeypatch: pytest.MonkeyPatch,
1883+
) -> None:
18591884
original_setstate = Morsel.__setstate__ # type: ignore[attr-defined]
18601885

18611886
def _mock_setstate(self: Morsel[str], state: dict[str, str]) -> None:
18621887
if any(ord(c) < 32 for c in state.get("value", "")):
18631888
raise CookieError()
18641889
original_setstate(self, state)
18651890

1866-
with patch(
1891+
monkeypatch.setattr(
18671892
"aiohttp._cookie_helpers.Morsel.__setstate__",
1868-
autospec=True,
1869-
side_effect=_mock_setstate,
1870-
):
1871-
yield
1893+
_mock_setstate,
1894+
)
18721895

18731896

1874-
def test_cookie_helpers_cve_fallback(mock_strict_morsel: None) -> None:
1897+
@pytest.mark.usefixtures("mock_strict_morsel")
1898+
def test_cookie_helpers_cve_fallback() -> None:
18751899
m: Morsel[str] = Morsel()
18761900
assert helpers._safe_set_morsel_state(m, "k", "v\n", "v\\012") is True
18771901
assert m.value == "v\\012"

0 commit comments

Comments
 (0)