Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGES/11178.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``Cookie`` header parsing to treat attribute names as regular cookies per :rfc:`6265#section-5.4` -- by :user:`bdraco`.
63 changes: 61 additions & 2 deletions aiohttp/_cookie_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@

from .log import internal_logger

__all__ = ("parse_cookie_headers", "preserve_morsel_with_coded_value")
__all__ = (
"parse_set_cookie_headers",
"parse_cookie_header",
"preserve_morsel_with_coded_value",
)

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


def parse_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
def parse_cookie_header(header: str) -> List[Tuple[str, Morsel[str]]]:
"""
Parse a Cookie header according to RFC 6265 Section 5.4.

Cookie headers contain only name-value pairs separated by semicolons.
There are no attributes in Cookie headers - even names that match
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.

Args:
header: The Cookie header value to parse

Returns:
List of (name, Morsel) tuples for compatibility with SimpleCookie.update()
"""
if not header:
return []

cookies: List[Tuple[str, 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

key = match.group("key")
value = match.group("val") or ""
i = match.end(0)

# Validate the name
if not key or not _COOKIE_NAME_RE.match(key):
internal_logger.warning("Can not load cookie: Illegal cookie name %r", key)
continue

# Create new morsel
morsel: Morsel[str] = 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
# setting protected attributes directly and unlikely to change since it would
# break pickling.
morsel.__setstate__( # type: ignore[attr-defined]
{"key": key, "value": _unquote(value), "coded_value": value}
)

cookies.append((key, morsel))

return cookies


def parse_set_cookie_headers(headers: Sequence[str]) -> List[Tuple[str, Morsel[str]]]:
"""
Parse cookie headers using a vendored version of SimpleCookie parsing.

Expand Down
4 changes: 2 additions & 2 deletions aiohttp/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from multidict import CIMultiDict
from yarl import URL

from ._cookie_helpers import parse_cookie_headers
from ._cookie_helpers import parse_set_cookie_headers
from .typedefs import LooseCookies

if TYPE_CHECKING:
Expand Down Expand Up @@ -198,7 +198,7 @@ def update_cookies_from_headers(
self, headers: Sequence[str], response_url: URL
) -> None:
"""Update cookies from raw Set-Cookie headers."""
if headers and (cookies_to_update := parse_cookie_headers(headers)):
if headers and (cookies_to_update := parse_set_cookie_headers(headers)):
self.update_cookies(cookies_to_update, response_url)

@abstractmethod
Expand Down
14 changes: 9 additions & 5 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
from yarl import URL

from . import hdrs, helpers, http, multipart, payload
from ._cookie_helpers import parse_cookie_headers, preserve_morsel_with_coded_value
from ._cookie_helpers import (
parse_cookie_header,
parse_set_cookie_headers,
preserve_morsel_with_coded_value,
)
from .abc import AbstractStreamWriter
from .client_exceptions import (
ClientConnectionError,
Expand Down Expand Up @@ -376,9 +380,9 @@ def cookies(self) -> SimpleCookie:
if self._raw_cookie_headers is not None:
# Parse cookies for response.cookies (SimpleCookie for backward compatibility)
cookies = SimpleCookie()
# Use parse_cookie_headers for more lenient parsing that handles
# Use parse_set_cookie_headers for more lenient parsing that handles
# malformed cookies better than SimpleCookie.load
cookies.update(parse_cookie_headers(self._raw_cookie_headers))
cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
self._cookies = cookies
else:
self._cookies = SimpleCookie()
Expand Down Expand Up @@ -1093,8 +1097,8 @@ def update_cookies(self, cookies: Optional[LooseCookies]) -> None:

c = SimpleCookie()
if hdrs.COOKIE in self.headers:
# parse_cookie_headers already preserves coded values
c.update(parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),)))
# parse_cookie_header for RFC 6265 compliant Cookie header parsing
c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
del self.headers[hdrs.COOKIE]

if isinstance(cookies, Mapping):
Expand Down
9 changes: 5 additions & 4 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from yarl import URL

from . import hdrs
from ._cookie_helpers import parse_cookie_headers
from ._cookie_helpers import parse_cookie_header
from .abc import AbstractStreamWriter
from .helpers import (
_SENTINEL,
Expand Down Expand Up @@ -589,9 +589,10 @@ def cookies(self) -> Mapping[str, str]:

A read-only dictionary-like object.
"""
# Use parse_cookie_headers for more lenient parsing that accepts
# special characters in cookie names (fixes #2683)
parsed = parse_cookie_headers((self.headers.get(hdrs.COOKIE, ""),))
# Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
# that accepts special characters in cookie names (fixes #2683)
parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
# Extract values from Morsel objects
return MappingProxyType({name: morsel.value for name, morsel in parsed})

@reify
Expand Down
Loading
Loading