Skip to content

fix: harden HTTP response body framing - #2653

Open
zyuhao7 wants to merge 5 commits into
drogonframework:masterfrom
zyuhao7:bugfix/response-framing
Open

zyuhao7 wants to merge 5 commits into
drogonframework:masterfrom
zyuhao7:bugfix/response-framing

Conversation

@zyuhao7

@zyuhao7 zyuhao7 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The response parser trusted framing fields a proxy may interpret differently, so a crafted or broken response could desynchronize a keep-alive or pipelined connection. This aligns the parser with RFC 9112 section 6.3:

  • Reject repeated Content-Length fields and any response carrying both Content-Length and Transfer-Encoding.
  • Validate Content-Length as 1*DIGIT (RFC 9110 section 8.6) via the shared HttpUtils::parseContentLength helper.
  • Detect chunked as the final transfer coding case-insensitively, including transfer parameters.
  • Consume the trailer section after the last chunk (RFC 9112 section 7.1); previously one line was consumed and the rest was parsed as the next response.
  • Treat 304 and 1xx responses as bodyless. Informational responses are skipped by HttpClientImpl instead of being delivered as the final response.

This branch is stacked on #2652 (bugfix/2648-content-length-framing), which adds HttpUtils::parseContentLength and the request-side framing fix. Only the last commit, fix: harden HTTP response body framing, belongs to this PR; the first two come from #2652 and will drop out of the diff once it lands.

Test plan

  • New HttpResponseParserTest.cc: 11 cases covering malformed/duplicate/overflowing Content-Length, Content-Length + Transfer-Encoding, chunked case and parameters, trailers, 304/204, interim responses, and HEAD.
  • Full unit suite on the stacked branch: 1454 assertions in 83 test cases passed.
  • Integration suite against integration_test_server: 10574 assertions in 29 test cases passed.

Validate Content-Length as 1*DIGIT inside HttpRequestImpl::addHeader, the
single place byte-level request headers enter the message:

* An empty value, a numeric prefix such as "10x", or a value beyond the
  size_t range is invalid framing (RFC 9110 section 8.6). Previously an
  empty value was indistinguishable from an absent header, so the bytes
  after the header section were parsed as the next request.
* A repeated Content-Length whose value differs from the one already
  received is rejected instead of silently keeping the first one
  (RFC 9112 section 6.3), closing a CL.CL request smuggling vector.
* A header line with whitespace between the field name and the colon is
  rejected with 400, as required by RFC 9112 section 5.1.

The parsed value is cached in contentLengthHeaderValue_, so the parser no
longer carries its own copy of the digit scan.

Fixes drogonframework#2648
The request parser needs the same 1*DIGIT validation, so share one
implementation instead of keeping separate copies that can diverge.
@zyuhao7
zyuhao7 force-pushed the bugfix/response-framing branch from 41cfd0c to cfeb398 Compare October 30, 2026 06:34
Comment thread lib/src/HttpRequestImpl.cc Outdated
{
size_t existingLength = 0;
return parseContentLength(it->second, existingLength) &&
existingLength == parsedLength;

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.

[P2] Compare duplicate values against the cached length

contentLengthHeaderValue_ already holds the parsed value from the first Content-Length field. Re-parsing it->second for every duplicate can repeatedly scan a potentially long attacker-controlled value, especially when followed by many short duplicate fields. Could we compare parsedLength directly with the cached value here, after checking that the optional is set?

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.

Fixed in #2652 (7a5d1e9): the duplicate is now compared against contentLengthHeaderValue_ without re-parsing the stored string. Branch restacked.

Picks up the OpenSSL 4.x build fixes (an-tao/trantor#423, drogonframework#422), which
the macOS 15 CI needs because Homebrew now ships OpenSSL 4.1.
@zyuhao7
zyuhao7 force-pushed the bugfix/response-framing branch from cfeb398 to 69ee62b Compare October 31, 2026 01:46
The first field already caches its parsed length in
contentLengthHeaderValue_, so compare against that instead of scanning
the stored string again for every repeated field.
A response parser that accepts ambiguous or malformed framing can
disagree with an intermediary about where the body ends, desynchronizing
keep-alive and pipelined connections.

- Validate Content-Length as 1*DIGIT, rejecting signs, whitespace,
  trailing junk and overflow.
- Reject repeated Content-Length fields and Content-Length combined with
  Transfer-Encoding.
- Detect chunked case-insensitively as the final transfer coding.
- Skip the trailer section after the last chunk instead of leaving it in
  the buffer as the start of the next response.
- Treat 304 and 1xx responses as bodyless and skip informational
  responses in the client.
@zyuhao7
zyuhao7 force-pushed the bugfix/response-framing branch from 69ee62b to 7984589 Compare October 31, 2026 02:20

This branch has not been deployed

No deployments
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