Skip to content

Fix CookieError crash with control characters on CPython builds with CVE-2026-3644 patch - #12395

Merged
Dreamsorcerer merged 17 commits into
aio-libs:masterfrom
rodrigobnogueira:fix-cookie-ctl-chars
May 13, 2026
Merged

Fix CookieError crash with control characters on CPython builds with CVE-2026-3644 patch#12395
Dreamsorcerer merged 17 commits into
aio-libs:masterfrom
rodrigobnogueira:fix-cookie-ctl-chars

Conversation

@rodrigobnogueira

@rodrigobnogueira rodrigobnogueira commented Apr 19, 2026

Copy link
Copy Markdown
Member

What do these changes do?

CPython builds that include the CVE-2026-3644 patch add strict validation to Morsel.__setstate__ that rejects values containing ASCII control characters. This causes aiohttp's cookie parser to crash with CookieError in two scenarios:

  1. Octal escape sequences_unquote decodes sequences like \012 to literal \n, which __setstate__ then rejects.
  2. Literal control characters — A raw header containing e.g. \x07 (BEL) is unsalvageable since both the decoded value and the coded_value contain the control character.

This PR adds a _safe_set_morsel_state helper that:

  • Wraps __setstate__ and catches CookieError
  • For case 1 and case 2: returns False so callers gracefully skip the unsalvageable cookie instead of crashing

Are there changes in behavior for the user?

Yes. On CPython builds with the CVE-2026-3644 patch, when a server sends a cookie containing a control character:

  • Octal-escaped control chars (e.g. \012): the cookie is silently skipped
  • Literal control chars (e.g. \x07): the cookie is silently skipped

In both cases the parser no longer crashes. On CPython builds without the patch, behavior is unchanged.

Is it a substantial burden for the maintainers to support this?

No. The helper is a thin wrapper around the existing __setstate__ calls, uses the same patterns already established in the codebase, and is well-tested across both patched and unpatched CPython builds.

Related issue number

N/A — This addresses a crash introduced by CPython's CVE-2026-3644 security patch.

Checklist

  • I think the code is well written
  • Unit tests for the changes exist
  • Documentation reflects the changes
  • If you provide code modification, please add yourself to CONTRIBUTORS.txt
  • Add a new news fragment into the CHANGES/ folder

@codecov

codecov Bot commented Apr 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.94%. Comparing base (9bc498b) to head (d4dba3f).
⚠️ Report is 9 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #12395   +/-   ##
=======================================
  Coverage   98.94%   98.94%           
=======================================
  Files         131      131           
  Lines       46558    46610   +52     
  Branches     2409     2412    +3     
=======================================
+ Hits        46068    46120   +52     
  Misses        367      367           
  Partials      123      123           
Flag Coverage Δ
Autobahn 22.42% <16.12%> (-0.01%) ⬇️
CI-GHA 98.91% <96.77%> (-0.01%) ⬇️
OS-Linux 98.66% <96.77%> (-0.01%) ⬇️
OS-Windows 97.02% <96.77%> (-0.01%) ⬇️
OS-macOS 97.93% <96.77%> (+<0.01%) ⬆️
Py-3.10.11 97.42% <96.77%> (+<0.01%) ⬆️
Py-3.10.20 97.91% <96.77%> (-0.01%) ⬇️
Py-3.11.15 98.16% <96.77%> (-0.01%) ⬇️
Py-3.11.9 97.68% <96.77%> (-0.01%) ⬇️
Py-3.12.10 97.77% <96.77%> (+<0.01%) ⬆️
Py-3.12.13 98.25% <96.77%> (-0.01%) ⬇️
Py-3.13.13 98.47% <90.32%> (-0.02%) ⬇️
Py-3.14.4 98.43% <90.32%> (-0.03%) ⬇️
Py-3.14.5t 97.54% <90.32%> (-0.02%) ⬇️
Py-pypy3.11.15-7.3.21 97.41% <96.77%> (-0.02%) ⬇️
VM-macos 97.93% <96.77%> (+<0.01%) ⬆️
VM-ubuntu 98.66% <96.77%> (-0.01%) ⬇️
VM-windows 97.02% <96.77%> (-0.01%) ⬇️
cython-coverage 37.95% <14.51%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label Apr 19, 2026
@codspeed-hq

codspeed-hq Bot commented Apr 19, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 8.47%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

#### 🎉 Hooray! `pytest-codspeed` just leveled up to 5.0.1!

A heads-up, this is a breaking change and it might affect your current performance baseline a bit. But here's the exciting part - it's packed with new, cool features and promises improved result stability 🥳!
Curious about what's new? Visit our releases page to delve into all the awesome details about this new version.

⚡ 1 improved benchmark
✅ 71 untouched benchmarks
⏩ 69 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
test_read_large_binary_websocket_messages 48.9 µs 45.1 µs +8.47%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing rodrigobnogueira:fix-cookie-ctl-chars (d4dba3f) with master (65b42bb)2

Open in CodSpeed

Footnotes

  1. 69 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on master (302511f) during the generation of this report, so 65b42bb was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@bdraco bdraco added backport-3.13 backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot labels Apr 19, 2026
@bdraco

bdraco commented Apr 19, 2026

Copy link
Copy Markdown
Member

Tested with Python 3.13.3 and control chars seem to be accepted. Version might need to be narrowed in the summary

@bdraco

bdraco commented Apr 19, 2026

Copy link
Copy Markdown
Member

python/cpython@57e88c1

looks like something like parse_set_cookie_headers(['name="a\x07b"']) would still raise on a Python with the cve patch landed

@Dreamsorcerer

Copy link
Copy Markdown
Member

It also restores the control character test cases that were previously masked.

I think you mean removed. I could see no reason that CTL characters were supposed to be allowed, the test was a generic test of octal unquoting. So my assumption is that the tests arbitrarily chose CTL characters and they were not a deliberate choice, nor did it suggest real cookies would have such characters. It seems fine to me for them to be rejected as a security concern.

rodrigobnogueira and others added 2 commits April 19, 2026 23:50
…back

- _safe_set_morsel_state now returns bool; callers skip unsalvageable cookies
- Handles both octal-decoded CTL chars and literal CTL chars in raw headers
- Added tests for literal control character edge case (bdraco feedback)
- Updated version wording to reference CVE-2026-3644 patch, not Python 3.13+
- Reworded test docstrings per Dreamsorcerer feedback
@rodrigobnogueira

Copy link
Copy Markdown
Member Author

It also restores the control character test cases that were previously masked.

I think you mean removed. I could see no reason that CTL characters were supposed to be allowed, the test was a generic test of octal unquoting. So my assumption is that the tests arbitrarily chose CTL characters and they were not a deliberate choice, nor did it suggest real cookies would have such characters. It seems fine to me for them to be rejected as a security concern.

Hello @Dreamsorcerer , the tests now describe what they actually verify (graceful handling vs crashing) without implying CTL chars should be allowed.
For context, this crash was originally surfaced by yarl's downstream CI on Python 3.13, where the "Aiohttp tests (3.13)" job failed with CookieError: Control characters are not allowed in cookies on 3 tests.

- Add 'unsalvageable' to docs spelling wordlist (fixes linter)
- Add test_parse_cookie_header_literal_ctl_chars for Cookie header path
- Remove artificial test_preserve_morsel_with_coded_value_literal_ctl_chars
  (a Morsel with control chars can't be constructed through normal APIs)
@rodrigobnogueira

Copy link
Copy Markdown
Member Author

I added a few extra tests to fix the missing coverage reported by Codecov.

@rodrigobnogueira

Copy link
Copy Markdown
Member Author

Pin pyupgrade to python3.13 to work around a crash introduced in Python 3.14 alpha (tokenize.cookie_re was changed from a string to a bytes pattern in 3.14, causing pyupgrade to fail with TypeError: cannot use a bytes pattern on a string-like object when run under 3.14). This is a pyupgrade bug

@webknjaz

Copy link
Copy Markdown
Member

Wow, looks like pyupgrade still targets the Python 3.7 syntax. cc @Dreamsorcerer do we still need it that low?


Would it make sense to bump the tool's version in a standalone PR?

@webknjaz

Copy link
Copy Markdown
Member

@rodrigobnogueira I haven't found any pyupgrade issue filed upstream? I recommend creating it there and linking from the commit or at least anywhere so it'd be clearer what problem you're referring to. If you can — it'd be a good idea to submit a fix upstream.

@webknjaz webknjaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below are a few notes/suggestions, not a full review.

Comment thread CHANGES/12395.bugfix.rst Outdated
Comment thread CHANGES/12395.bugfix.rst Outdated
Comment thread tests/test_cookie_helpers.py
Comment thread tests/test_cookie_helpers.py
Comment thread tests/test_cookie_helpers.py Outdated
Comment thread tests/test_cookie_helpers.py Outdated
Comment thread tests/test_cookie_helpers.py Outdated
Comment thread tests/test_cookie_helpers.py Outdated
Comment thread tests/test_cookie_helpers.py Outdated
@Dreamsorcerer

Copy link
Copy Markdown
Member

Wow, looks like pyupgrade still targets the Python 3.7 syntax. cc @Dreamsorcerer do we still need it that low?

I'm going to replace the whole setup with ruff in the near future, so haven't really looked at it. I did a manual upgrade with ruff a couple of months ago, though I only applied the safe fixes at that time, so still a few outdated bits in the code currently.

@Dreamsorcerer

Copy link
Copy Markdown
Member

For context, this crash was originally surfaced by yarl's downstream CI on Python 3.13, where the "Aiohttp tests (3.13)" job failed with CookieError: Control characters are not allowed in cookies on 3 tests.

Yeah, I saw that. That's just because those tests are outdated, right? We can backport the change to 3.13 branch to resolve that.

@Dreamsorcerer

Copy link
Copy Markdown
Member

For context, this crash was originally surfaced by yarl's downstream CI on Python 3.13, where the "Aiohttp tests (3.13)" job failed with CookieError: Control characters are not allowed in cookies on 3 tests.

Yeah, I saw that. That's just because those tests are outdated, right? We can backport the change to 3.13 branch to resolve that.

#12401

Comment thread aiohttp/_cookie_helpers.py Outdated
@rodrigobnogueira
rodrigobnogueira force-pushed the fix-cookie-ctl-chars branch 3 times, most recently from 861e965 to d171cbd Compare April 21, 2026 04:02
- 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
@rodrigobnogueira rodrigobnogueira changed the title Fix CookieError crash with control characters on Python 3.13 Fix CookieError crash with control characters on CPython builds with CVE-2026-3644 patch Apr 25, 2026
Comment thread aiohttp/_cookie_helpers.py Outdated
Drop _safe_set_morsel_state helper and inline the try/except
at each call site, as suggested by Dreamsorcerer — the function
was simplified to a trivial wrapper and no longer warrants its
own definition.
@Princemachiavelli

Copy link
Copy Markdown

The new tests pass but don't the existing octal tests still expect decoding while they should be skipped now if CookieError exception is caught? (Actually I'd expect the behavior to depend on which Python version is used so should the tests just be disabled?)

> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[name="\\012newline\\012"-name-\nnewline\n-"\\012newline\\012"] - assert 0 == 1
> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[tab="\\011separated\\011values"-tab-\tseparated\tvalues-"\\011separated\\011values"] - assert 0 == 1
> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[complex="\\042quoted\\042 text with \\012 newline"-complex-"quoted" text with \n newline-"\\042quoted\\042 text with \\012 newline"] - assert 0 == 1

This is on Python 3.13.13 and applied this PR to aiohttp 3.13.5.

@rodrigobnogueira

Copy link
Copy Markdown
Member Author

The new tests pass but don't the existing octal tests still expect decoding while they should be skipped now if CookieError exception is caught? (Actually I'd expect the behavior to depend on which Python version is used so should the tests just be disabled?)

> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[name="\\012newline\\012"-name-\nnewline\n-"\\012newline\\012"] - assert 0 == 1
> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[tab="\\011separated\\011values"-tab-\tseparated\tvalues-"\\011separated\\011values"] - assert 0 == 1
> FAILED tests/test_cookie_helpers.py::test_parse_set_cookie_headers_uses_unquote_with_octal[complex="\\042quoted\\042 text with \\012 newline"-complex-"quoted" text with \n newline-"\\042quoted\\042 text with \\012 newline"] - assert 0 == 1

This is on Python 3.13.13 and applied this PR to aiohttp 3.13.5.

Thanks for testing! The failures are because you applied the PR to the released 3.13.5, but the test cases with control-char octals (\012, \011) were already replaced with printable-only octals on master in #12350, which has been backported to the 3.13 (#12401) and 3.14 (#12351) branches but not released yet. When this PR is backported it will land on those branches where the conflict is already resolved.

Comment thread aiohttp/_cookie_helpers.py
Comment thread aiohttp/_cookie_helpers.py
@Dreamsorcerer
Dreamsorcerer merged commit 7eb0e80 into aio-libs:master May 13, 2026
46 checks passed
@patchback

patchback Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Backport to 3.14: 💚 backport PR created

✅ Backport PR branch: patchback/backports/3.14/7eb0e8029cdb270a98a203bc5d4bad32cdc2c19c/pr-12395

Backported as #12533

🤖 @patchback
I'm built with octomachinery and
my source is open — https://github.com/sanitizers/patchback-github-app.

Dreamsorcerer pushed a commit that referenced this pull request May 13, 2026
…l characters on CPython builds with CVE-2026-3644 patch (#12533)

**This is a backport of PR #12395 as merged into master
(7eb0e80).**

Co-authored-by: Rodrigo Nogueira <rodrigo.b.nogueira@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-3.14 Trigger automatic backporting to the 3.14 release branch by Patchback robot bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants