Skip to content

Commit 915338c

Browse files
Fix cookie header parser ignoring reserved names (#11178)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 85b0df4 commit 915338c

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
@@ -22,7 +22,7 @@
2222
from multidict import CIMultiDict
2323
from yarl import URL
2424

25-
from ._cookie_helpers import parse_cookie_headers
25+
from ._cookie_helpers import parse_set_cookie_headers
2626
from .typedefs import LooseCookies
2727

2828
if TYPE_CHECKING:
@@ -194,7 +194,7 @@ def update_cookies_from_headers(
194194
self, headers: Sequence[str], response_url: URL
195195
) -> None:
196196
"""Update cookies from raw Set-Cookie headers."""
197-
if headers and (cookies_to_update := parse_cookie_headers(headers)):
197+
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
198198
self.update_cookies(cookies_to_update, response_url)
199199

200200
@abstractmethod

aiohttp/client_reqrep.py

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

3232
from . import hdrs, helpers, http, multipart, payload
33-
from ._cookie_helpers import parse_cookie_headers, preserve_morsel_with_coded_value
33+
from ._cookie_helpers import (
34+
parse_cookie_header,
35+
parse_set_cookie_headers,
36+
preserve_morsel_with_coded_value,
37+
)
3438
from .abc import AbstractStreamWriter
3539
from .client_exceptions import (
3640
ClientConnectionError,
@@ -313,9 +317,9 @@ def cookies(self) -> SimpleCookie:
313317
if self._raw_cookie_headers is not None:
314318
# Parse cookies for response.cookies (SimpleCookie for backward compatibility)
315319
cookies = SimpleCookie()
316-
# Use parse_cookie_headers for more lenient parsing that handles
320+
# Use parse_set_cookie_headers for more lenient parsing that handles
317321
# malformed cookies better than SimpleCookie.load
318-
cookies.update(parse_cookie_headers(self._raw_cookie_headers))
322+
cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
319323
self._cookies = cookies
320324
else:
321325
self._cookies = SimpleCookie()
@@ -1014,8 +1018,8 @@ def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
10141018

10151019
c = SimpleCookie()
10161020
if hdrs.COOKIE in self.headers:
1017-
# parse_cookie_headers already preserves coded values
1018-
c.update(parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),)))
1021+
# parse_cookie_header for RFC 6265 compliant Cookie header parsing
1022+
c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
10191023
del self.headers[hdrs.COOKIE]
10201024

10211025
if isinstance(cookies, Mapping):

aiohttp/web_request.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from yarl import URL
2929

3030
from . import hdrs
31-
from ._cookie_helpers import parse_cookie_headers
31+
from ._cookie_helpers import parse_cookie_header
3232
from .abc import AbstractStreamWriter
3333
from .helpers import (
3434
_SENTINEL,
@@ -556,9 +556,10 @@ def cookies(self) -> Mapping[str, str]:
556556
557557
A read-only dictionary-like object.
558558
"""
559-
# Use parse_cookie_headers for more lenient parsing that accepts
560-
# special characters in cookie names (fixes #2683)
561-
parsed = parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),))
559+
# Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
560+
# that accepts special characters in cookie names (fixes #2683)
561+
parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
562+
# Extract values from Morsel objects
562563
return MappingProxyType({name: morsel.value for name, morsel in parsed})
563564

564565
@reify

0 commit comments

Comments
 (0)