Reject HTTP/1 header forms that desync framing, drop chunked trailers - #3521
Reject HTTP/1 header forms that desync framing, drop chunked trailers#3521chenBright wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The current changes introduce a few correctness/test-isolation issues (notably test dependence on default gflags and inconsistent handling of HTAB as OWS in Content-Length whitespace states) that should be fixed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens bRPC’s embedded HTTP/1 parser to eliminate several parsing discrepancies that can lead to request smuggling / response desync in proxy chains, and it drops chunked trailers so they can’t be merged into the main header map.
Changes:
- Add strict/compat toggles for header token parsing (
http_strict_header_token) and obs-fold handling for framing headers (http_allow_obs_fold). - Tighten parsing of
Content-LengthandTransfer-Encodingto reject ambiguous forms (e.g., interior whitespace inContent-Length, framing-header obs-fold) and correctly handleTransfer-Encodinglists with OWS after commas. - Discard chunked trailers in
HttpMessagecallbacks to prevent trailer fields from being merged into the header map; add extensive unit tests covering the above.
File summaries
| File | Description |
|---|---|
test/brpc_http_message_unittest.cpp |
Adds regression tests for strict header tokens, framing-header whitespace/obs-fold rejection, TE list parsing, and dropping chunked trailers. |
src/brpc/details/http_parser.h |
Adds two parser config bitfields to control obs-fold and strict header token handling. |
src/brpc/details/http_parser.cpp |
Implements strict token parsing for header field names, more robust Content-Length parsing, framing-header obs-fold rejection, and correct TE list parsing with OWS after comma. |
src/brpc/details/http_message.cpp |
Adds gflags for new parser behaviors, wires them into HttpMessage parser init, and drops trailer callbacks when F_TRAILING is set. |
Review details
Suppressed comments (3)
src/brpc/details/http_parser.cpp:1699
- When transitioning from Content-Length digits to the trailing-whitespace state, only SP is recognized. If you intend to support RFC 7230 OWS, this should also treat HTAB as whitespace so that trailing tabs don’t incorrectly trigger HPE_INVALID_CONTENT_LENGTH.
if (ch == ' ') {
parser->header_state = h_content_length_ws;
break;
}
src/brpc/details/http_parser.cpp:1726
- In the Content-Length trailing-whitespace state, only SP is skipped. If HTAB is treated as OWS elsewhere (e.g., after ':'), it should likely be accepted here too (or the comment should be updated to avoid claiming OWS support).
case h_content_length_ws:
if (ch == ' ') break;
SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
test/brpc_http_message_unittest.cpp:547
- This test asserts --http_allow_obs_fold is false by default, which can break when the test binary is executed with a non-default flag value. Set the flag explicitly (and save/restore it) so the test is independent of external flag configuration.
ASSERT_FALSE(brpc::FLAGS_http_allow_obs_fold);
ASSERT_EQ(brpc::HPE_INVALID_HEADER_TOKEN, ParseHttpErrno(folded_te_value));
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
What problem does this PR solve?
Issue Number: resolve
Problem Summary:
Four header forms are parsed here in a way a conforming front-end does not, and
one legal form is not parsed at all. In a
client -> proxy -> bRPCchain bothparties parse the same bytes independently, so every disagreement about where a
message ends turns the tail of message N into the head of message N+1 for one of
them. The primitive behind request smuggling and response desync.
Content-Length : 5— this fork'sTOKEN(c)maps SP to itself. Upstreamhas a
STRICT_TOKEN()and aHTTP_PARSER_STRICTbranch. Both were droppedhere, leaving the lenient path as the only path. A space before the colon
therefore keeps
header_stateon the framing header, so the value stilldrives our framing while the field name we report is
"Content-Length ".RFC 7230 3.2.6 excludes SP from
tchar, and 3.2.4 forbids whitespace beforethe colon.
Content-Length: 1 3— interior blanks were skipped, so we read 13 wherea proxy reads 1 or rejects. Same class as CVE-2022-32213, fixed upstream by
http-parser nodejs/http-parser@01da95f and nodejs/http-parser@cd88eef.
Neither was picked up here.
Transfer-Encoding:\r\n chunked,Content-Length:\r\n 5— both obs-foldstates (RFC 7230 3.2.4) carry
header_stateacross the fold, so a foldedframing value still drove our framing while a front-end that does not unfold
sees a different message.
chunked trailers — after the last chunk the parser sets
F_TRAILINGandreturns to
s_header_field_start, so trailer fields reach the sameon_header_field/on_header_valuecallbacks as the header section. Neitherlooked at
F_TRAILING, so a trailer landed in the header map like a realheader: appended to an existing entry of the same name, or added as a new one.
A front-end only filters the header section, so any chunked request could put
an
AuthorizationorX-Forwarded-Forpast it:Transfer-Encoding: gzip, chunked— found while auditing theTOKEN()call sites for (1).
TOKEN(' ')is truthy, which made theelse if (c == ' ' || c == '\t')arm ofh_matching_transfer_encoding_token_startdead code: the OWS after the commastarted a new coding name, so
chunkednever matched. This is the formRFC 7230 7 spells out. Requests were rejected with
HPE_INVALID_TRANSFER_ENCODINGper RFC 7230 3.3.3, and responses were read toEOF instead of as chunked.
What is changed and the side effects?
Changed:
Reintroduce
STRICT_TOKEN(). This fork'stokens[]already omits SP asRFC 7230 3.2.6 does, so the strict form is the plain lookup and
TOKEN()isthe leniency layered on top.
Reject SP before the colon of
Content-Length,Transfer-Encoding,ConnectionandUpgrade. Those four are the only terminalheader_statesreachable in that switch arm. Any other name is
h_generalor stillmatching, and keeps the historical leniency unless the new flag is on.
Backport nodejs/http-parser@01da95f and nodejs/http-parser@cd88eef.
Reject obs-fold of a value whose
header_statedecides where the body ends(
is_framing_header_state(), i.e. the Content-Length and Transfer-Encodingstates). Both fold states need the check:
s_header_value_lwsfor a valuethat has started,
s_header_value_discard_lwsfor one that has not.Use
STRICT_TOKEN()unconditionally inh_matching_transfer_encoding_token_start,as upstream does, so the OWS after a comma is skipped instead of starting a coding name.
Add the
h_content_length*,h_transfer_encoding,h_upgradeandh_matching_transfer_encoding_*cases missing fromhttp_parser_header_state_name(), which printedh_unknownfor them.on_header_fieldandon_header_valuereturn early whenF_TRAILINGisset, so trailers are discarded rather than merged.
parser->flagsiscleared at the start of each message, so the check is scoped to one trailer.
Side effects:
Performance effects:
Breaking backward compatibility:
Check List: