Skip to content

Reject all RFC 9110 forbidden control characters in outbound headers#12689

Open
rodrigobnogueira wants to merge 2 commits into
aio-libs:masterfrom
rodrigobnogueira:harden/reject-forbidden-ctls-in-headers
Open

Reject all RFC 9110 forbidden control characters in outbound headers#12689
rodrigobnogueira wants to merge 2 commits into
aio-libs:masterfrom
rodrigobnogueira:harden/reject-forbidden-ctls-in-headers

Conversation

@rodrigobnogueira
Copy link
Copy Markdown
Member

@rodrigobnogueira rodrigobnogueira commented May 23, 2026

What do these changes do?

Tighten the outbound header serializer so it rejects every ASCII control
character that :rfc:9110#section-5.5 and :rfc:9112#section-4 forbid in
header field-values and reason-phrases (0x00-0x08, 0x0A-0x1F,
0x7F), not just CR, LF and NUL.

  • _safe_header() in aiohttp/http_writer.py now uses a compiled regex
    covering the full forbidden set.
  • _write_str_raise_on_nlcr() in aiohttp/_http_writer.pyx uses the
    equivalent inequality (ch < 0x20 and ch != 0x09) or ch == 0x7F.
  • HTAB (0x09) and SP (0x20) remain permitted, matching RFC 9110.
  • Existing CR/LF/NUL tests preserved; new parametrized tests cover the
    broader forbidden set across status lines, header names, and field
    values, plus a positive test for HTAB.

This aligns outbound validation with inbound strict parsing.

Are there changes in behavior for the user?

Yes. Applications that placed bare control characters (other than HTAB)
into outbound headers will now get a ValueError instead of silently
emitting non-RFC-compliant bytes. The error message changes from
"Newline, carriage return, or null byte detected in headers." to
"Forbidden control character detected in headers."

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

No. It is a small, contained change in two files (the pure-Python and
Cython serializers) with mirrored logic.

Related issue number

N/A

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 (already listed)
  • Add a new news fragment into the CHANGES/ folder

@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label May 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.95%. Comparing base (d2c203f) to head (224ab50).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #12689      +/-   ##
==========================================
+ Coverage   98.93%   98.95%   +0.02%     
==========================================
  Files         131      131              
  Lines       46688    46708      +20     
  Branches     2421     2421              
==========================================
+ Hits        46190    46220      +30     
+ Misses        374      366       -8     
+ Partials      124      122       -2     
Flag Coverage Δ
Autobahn 22.42% <39.13%> (+<0.01%) ⬆️
CI-GHA 98.92% <100.00%> (+0.01%) ⬆️
OS-Linux 98.67% <100.00%> (+0.01%) ⬆️
OS-Windows 97.04% <95.65%> (+<0.01%) ⬆️
OS-macOS 97.93% <95.65%> (+<0.01%) ⬆️
Py-3.10 98.15% <100.00%> (+<0.01%) ⬆️
Py-3.11 98.41% <100.00%> (+<0.01%) ⬆️
Py-3.12 98.50% <100.00%> (+<0.01%) ⬆️
Py-3.13 98.47% <100.00%> (-0.01%) ⬇️
Py-3.14 98.49% <100.00%> (+<0.01%) ⬆️
Py-3.14t 97.55% <95.65%> (-0.01%) ⬇️
Py-pypy-3.11 97.42% <100.00%> (?)
VM-macos 97.93% <95.65%> (+<0.01%) ⬆️
VM-ubuntu 98.67% <100.00%> (+0.01%) ⬆️
VM-windows 97.04% <95.65%> (+<0.01%) ⬆️
cython-coverage 37.97% <95.83%> (+0.02%) ⬆️

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.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 23, 2026

Merging this PR will not alter performance

✅ 72 untouched benchmarks
⏩ 72 skipped benchmarks1


Comparing rodrigobnogueira:harden/reject-forbidden-ctls-in-headers (224ab50) with master (d2c203f)

Open in CodSpeed

Footnotes

  1. 72 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.

Reflect the broader RFC 9110 §5.5 / RFC 9112 §4 forbidden-CTL rejection
in §5.2's components list, selection note, threat row 2.1, mitigation
rows 2.1/2.2/2.3/2.7/2.13, and the past-advisories recap. Adjust the
mermaid flow label from "reject CR/LF/NUL" to "reject forbidden CTLs"
and add the GHSA to the recap as the source of the tightening.
@rodrigobnogueira rodrigobnogueira marked this pull request as ready for review May 23, 2026 22:32
Comment thread aiohttp/_http_writer.pyx
Comment on lines +114 to +117
# RFC 9110 §5.5 / RFC 9112 §4: reject all ASCII control characters
# (0x00-0x08, 0x0A-0x1F, 0x7F) in headers, status lines, and reason
# phrases. HTAB (0x09) and SP (0x20) remain permitted.
if (ch < 0x20 and ch != 0x09) or ch == 0x7F:
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.

Suggested change
# RFC 9110 §5.5 / RFC 9112 §4: reject all ASCII control characters
# (0x00-0x08, 0x0A-0x1F, 0x7F) in headers, status lines, and reason
# phrases. HTAB (0x09) and SP (0x20) remain permitted.
if (ch < 0x20 and ch != 0x09) or ch == 0x7F:
# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5
# https://www.rfc-editor.org/info/rfc9112/#section-4-3
if (ch < 0x20 and ch != 0x09) or ch == 0x7F:

Comment thread THREAT_MODEL.md
| 2.1 | Header CR / LF / NUL injection | Both backends reject these bytes via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** |
| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects them at write-time as part of the status-line validation. | None. |
| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so CR / LF / NUL are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects these bytes earlier per RFC 3986. | None. |
| 2.1 | Header forbidden-CTL injection | Both backends reject the full RFC 9110 §5.5 / RFC 9112 §4 forbidden set (`0x00-0x08`, `0x0A-0x1F`, `0x7F`; HTAB and SP permitted) via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. Hardening tightened in GHSA-xjx4-5hx2-2cv8. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** |
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.

@rodrigobnogueira Going to need to be more careful here. GHSA-xjx4-5hx2-2cv8 isn't public. We've also not deemed it a vulnerability, so this won't be published. Should just reference the PR (and probably only need it in the recap section at the bottom, rather than in here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

2 participants