Skip to content

Commit a075bd6

Browse files
patchback[bot]bdracopre-commit-ci[bot]
authored
[PR #11178/915338c7 backport][3.13] Fix cookie header parser ignoring reserved names (#11182)
Co-authored-by: J. Nick Koston <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 058dced commit a075bd6

File tree

7 files changed

+556
-178
lines changed

7 files changed

+556
-178
lines changed

CHANGES/11178.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed ``Cookie`` header parsing to treat attribute names as regular cookies per :rfc:`6265#section-5.4` -- by :user:`bdraco`.

aiohttp/_cookie_helpers.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212

1313
from .log import internal_logger
1414

15-
__all__ = ("parse_cookie_headers", "preserve_morsel_with_coded_value")
15+
__all__ = (
16+
"parse_set_cookie_headers",
17+
"parse_cookie_header",
18+
"preserve_morsel_with_coded_value",
19+
)
1620

1721
# Cookie parsing constants
1822
# Allow more characters in cookie names to handle real-world cookies
@@ -153,7 +157,62 @@ def _unquote(value: str) -> str:
153157
return _unquote_sub(_unquote_replace, value)
154158

155159

156-
def parse_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
160+
def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
161+
"""
162+
Parse a Cookie header according to RFC 6265 Section 5.4.
163+
164+
Cookie headers contain only name-value pairs separated by semicolons.
165+
There are no attributes in Cookie headers - even names that match
166+
attribute names (like 'path' or 'secure') should be treated as cookies.
167+
168+
This parser uses the same regex-based approach as parse_set_cookie_headers
169+
to properly handle quoted values that may contain semicolons.
170+
171+
Args:
172+
header: The Cookie header value to parse
173+
174+
Returns:
175+
List of (name, Morsel) tuples for compatibility with SimpleCookie.update()
176+
"""
177+
if not header:
178+
return []
179+
180+
cookies: List[Tuple[str, Morsel[str]]] = []
181+
i = 0
182+
n = len(header)
183+
184+
while i < n:
185+
# Use the same pattern as parse_set_cookie_headers to find cookies
186+
match = _COOKIE_PATTERN.match(header, i)
187+
if not match:
188+
break
189+
190+
key = match.group("key")
191+
value = match.group("val") or ""
192+
i = match.end(0)
193+
194+
# Validate the name
195+
if not key or not _COOKIE_NAME_RE.match(key):
196+
internal_logger.warning("Can not load cookie: Illegal cookie name %r", key)
197+
continue
198+
199+
# Create new morsel
200+
morsel: Morsel[str] = Morsel()
201+
# Preserve the original value as coded_value (with quotes if present)
202+
# We use __setstate__ instead of the public set() API because it allows us to
203+
# bypass validation and set already validated state. This is more stable than
204+
# setting protected attributes directly and unlikely to change since it would
205+
# break pickling.
206+
morsel.__setstate__( # type: ignore[attr-defined]
207+
{"key": key, "value": _unquote(value), "coded_value": value}
208+
)
209+
210+
cookies.append((key, morsel))
211+
212+
return cookies
213+
214+
215+
def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
157216
"""
158217
Parse cookie headers using a vendored version of SimpleCookie parsing.
159218

aiohttp/abc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from multidict import CIMultiDict
2424
from yarl import URL
2525

26-
from ._cookie_helpers import parse_cookie_headers
26+
from ._cookie_helpers import parse_set_cookie_headers
2727
from .typedefs import LooseCookies
2828

2929
if TYPE_CHECKING:
@@ -198,7 +198,7 @@ def update_cookies_from_headers(
198198
self, headers: Sequence[str], response_url: URL
199199
) -> None:
200200
"""Update cookies from raw Set-Cookie headers."""
201-
if headers and (cookies_to_update := parse_cookie_headers(headers)):
201+
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
202202
self.update_cookies(cookies_to_update, response_url)
203203

204204
@abstractmethod

aiohttp/client_reqrep.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
from yarl import URL
3232

3333
from . import hdrs, helpers, http, multipart, payload
34-
from ._cookie_helpers import parse_cookie_headers, preserve_morsel_with_coded_value
34+
from ._cookie_helpers import (
35+
parse_cookie_header,
36+
parse_set_cookie_headers,
37+
preserve_morsel_with_coded_value,
38+
)
3539
from .abc import AbstractStreamWriter
3640
from .client_exceptions import (
3741
ClientConnectionError,
@@ -376,9 +380,9 @@ def cookies(self) -> SimpleCookie:
376380
if self._raw_cookie_headers is not None:
377381
# Parse cookies for response.cookies (SimpleCookie for backward compatibility)
378382
cookies = SimpleCookie()
379-
# Use parse_cookie_headers for more lenient parsing that handles
383+
# Use parse_set_cookie_headers for more lenient parsing that handles
380384
# malformed cookies better than SimpleCookie.load
381-
cookies.update(parse_cookie_headers(self._raw_cookie_headers))
385+
cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
382386
self._cookies = cookies
383387
else:
384388
self._cookies = SimpleCookie()
@@ -1093,8 +1097,8 @@ def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
10931097

10941098
c = SimpleCookie()
10951099
if hdrs.COOKIE in self.headers:
1096-
# parse_cookie_headers already preserves coded values
1097-
c.update(parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),)))
1100+
# parse_cookie_header for RFC 6265 compliant Cookie header parsing
1101+
c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
10981102
del self.headers[hdrs.COOKIE]
10991103

11001104
if isinstance(cookies, Mapping):

aiohttp/web_request.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from yarl import URL
3636

3737
from . import hdrs
38-
from ._cookie_helpers import parse_cookie_headers
38+
from ._cookie_helpers import parse_cookie_header
3939
from .abc import AbstractStreamWriter
4040
from .helpers import (
4141
_SENTINEL,
@@ -589,9 +589,10 @@ def cookies(self) -> Mapping[str, str]:
589589
590590
A read-only dictionary-like object.
591591
"""
592-
# Use parse_cookie_headers for more lenient parsing that accepts
593-
# special characters in cookie names (fixes #2683)
594-
parsed = parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),))
592+
# Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
593+
# that accepts special characters in cookie names (fixes #2683)
594+
parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
595+
# Extract values from Morsel objects
595596
return MappingProxyType({name: morsel.value for name, morsel in parsed})
596597

597598
@reify

0 commit comments

Comments
 (0)