Skip to content

fix(h1): require chunked as the final transfer-coding, trim OWS - #19

Merged
FlorentinDUBOIS merged 3 commits into
mainfrom
fix/transfer-encoding-ows
Jul 15, 2026
Merged

fix(h1): require chunked as the final transfer-coding, trim OWS#19
FlorentinDUBOIS merged 3 commits into
mainfrom
fix/transfer-encoding-ows

Conversation

@FlorentinDUBOIS

Copy link
Copy Markdown
Contributor

Summary

Make chunked-framing detection in process_headers RFC-correct.

Chunked framing was selected by a suffix-only compare on the raw Transfer-Encoding value, with no
OWS trim. Per RFC 9112 §6.1 (chunked must be the final transfer-coding) and RFC 9110 §5.6.3 (OWS),
this selects chunked only when the final comma-separated transfer-coding token equals chunked
(case-insensitive) after trimming OWS from the value and each token. When a Transfer-Encoding is
present but its final coding is not chunked, it now raises a parse error instead of leaving the
framing unresolved. Existing behavior (Content-Length elision, the multi-TE / CL+TE warnings) is
preserved when chunked is selected.

Fixes two issues in the old suffix check: values such as chunked\t or chunked were not
recognised as chunked, and xchunked false-positived as chunked.

Two small zero-copy helpers (trim_ows, ends_with_chunked_coding), no new deps.

Tests (tests/edge_cases.rs)

chunked, chunked\t, chunked, gzip, chunked → Chunked; chunked, gzip, identity,
xchunked → parse error; Content-Length + chunked\t → Chunked with Content-Length elided. All
17 crate tests pass; clippy --lib and fmt --check clean.

Notes for reviewers

  • clippy --all-targets -- -D warnings fails on this branch, but identically on unmodified
    v0.6.8 (pre-existing test-lint debt: unused_io_amount, etc.). clippy --lib is clean.
  • Headers are processed one block at a time, so a split Transfer-Encoding: gzip +
    Transfer-Encoding: chunked (two separate lines) is now rejected on the first line, while the
    combined Transfer-Encoding: gzip, chunked is accepted — flagging in case you'd prefer to
    coalesce same-name TE headers first. The reject-on-non-chunked-final rule also applies to
    responses (e.g. a close-delimited Transfer-Encoding: gzip response is rejected).

Chunked framing was selected by a suffix-only compare on the raw
Transfer-Encoding value, with no OWS trim, so values like `chunked\t`
(trailing tab), ` chunked ` (surrounding OWS), and `chunked, gzip`
(chunked not the final coding) were not recognised as chunked, and
`xchunked` false-positived as chunked.

Per RFC 9112 §6.1 (chunked must be the final transfer-coding) and
RFC 9110 §5.6.3 (OWS), select chunked only when the final comma-separated
transfer-coding token equals `chunked` (case-insensitive) after trimming
OWS from the value and each token. When a Transfer-Encoding is present but
its final coding is not chunked, raise a parse error rather than leaving
the framing unresolved. Content-Length elision and the multi-TE / CL+TE
warnings are preserved when chunked is selected.

Two small zero-copy helpers (trim_ows, ends_with_chunked_coding), no new
deps. Tests in tests/edge_cases.rs cover the accepted and rejected forms
and the Content-Length elision.

Signed-off-by: Florentin Dubois <florentin.dubois@clever.cloud>

@FlorentinDUBOIS FlorentinDUBOIS left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated cross-review (review / review-code / guidelines / simplify / security-review + Codex), adversarially verified. 2 findings survived (3 refuted). Posted as comments (self-authored PR).

Comment thread src/protocol/h1/parser/mod.rs Outdated
Comment thread src/protocol/h1/parser/mod.rs Outdated
…Transfer-Encoding

Addresses two review findings on the initial commit:

- process_headers serves both requests and responses, so the
  unconditional error on a non-chunked-final Transfer-Encoding wrongly
  rejected spec-valid close-delimited responses (e.g. a `gzip` response).
  Gate the reject on kawa.kind == Kind::Request; a response keeps the
  body size Content-Length processing produced (close-delimited when
  Empty), matching pre-fix behaviour.

- Repeated Transfer-Encoding field lines combine per RFC 9110 §5.3, so a
  split `gzip` then `chunked` is `gzip, chunked` (chunked is the final
  coding) and must not be rejected on the first line. Defer the reject
  via a pending flag that a later chunked-final line clears; decide once
  after the header-block loop. `chunked` then `identity` still rejects
  (request).

Tests (tests/edge_cases.rs): split gzip+chunked accepted, split
chunked+identity rejected, and a response with a non-chunked-final
Transfer-Encoding no longer errors.

Signed-off-by: Florentin Dubois <florentin.dubois@clever.cloud>
…ding

The previous commit set body_size = Chunked immediately on a chunked
Transfer-Encoding line and only deferred the reject decision, so a
response whose split Transfer-Encoding lines were `chunked` then
`identity` stayed mis-framed as chunked even though the combined final
coding is `identity` (close-delimited).

Resolve Transfer-Encoding once, up front: repeated field lines combine
(RFC 9110 §5.3), so the combined final transfer-coding is the final coding
of the last line. Pre-scan for presence and final-chunked, then process
Content-Length knowing a present Transfer-Encoding overrides it (RFC 9110
§6.3 -> drop every Content-Length), and decide framing after the loop:

- combined ends in chunked          -> Chunked;
- request, not chunked-final        -> reject (RFC 9112 §6.3);
- response, not chunked-final       -> close-delimited (body_size stays Empty).

This keeps the existing multiple_length_information behaviour (a differing
Content-Length after chunked lines is dropped, not an inconsistency error,
because Transfer-Encoding overrides Content-Length) while fixing the
response mis-framing.

Test (tests/edge_cases.rs): a response with split `chunked` then
`identity` Transfer-Encoding lines is now close-delimited
(body_size == Empty), not chunked; seen failing against the prior code.

Signed-off-by: Florentin Dubois <florentin.dubois@clever.cloud>
@FlorentinDUBOIS
FlorentinDUBOIS merged commit 79e2e3b into main Jul 15, 2026
12 checks passed
@FlorentinDUBOIS
FlorentinDUBOIS deleted the fix/transfer-encoding-ows branch July 15, 2026 08:56
@UncleJ4ck

Copy link
Copy Markdown

Hi @FlorentinDUBOIS, this PR matches a vulnerability report I sent privately to
security@clever.cloud on 2026-07-10, which did not get a reply. Same file and same
root cause: the suffix-only chunked compare with no OWS trim in
src/protocol/h1/parser/mod.rs, which let values like chunked\t, chunked and
chunked, identity through alongside a Content-Length. Paired with a backend that
trims OWS and honors Transfer-Encoding (Go net/http, Puma, h11, Hypercorn all do),
that is a CL.TE desync. My report also flagged it as a regression of #726, which this
commit's comment closes, and asked for exactly the fix you shipped: reject when a
Transfer-Encoding is present but does not resolve to a final chunked, rather than
falling back to Content-Length and forwarding a header you did not understand. Thanks
for fixing it quickly and at the class rather than just trimming the tab.

Two things:

  1. Advisory. kawa 0.7.0 and sozu 2.2.0 present this as an RFC-correctness change, so
    operators still on kawa <0.7.0 or sozu <2.2.0 have no signal that they should upgrade
    for a request-smuggling fix. Are you planning a GHSA or a CVE? If you would rather not
    file one, I have opened a RustSec advisory for the crate
    (Add advisory for kawa Transfer-Encoding request smuggling (fixed in 0.7.0) rustsec/advisory-db#3142, affected <0.7.0, patched 0.7.0) so downstream users get
    that signal. The RustSec maintainers have asked for a word from you before merging, so
    a comment either way on that PR would unblock it.

  2. Credit. Could the report be credited to unclej4ck (Aymane Mazguiti)?

Happy to share the full write-up and the reproduction. I have published a technical
post now that the fix is released, and I am glad to add any correction or statement
you want in it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants