Skip to content

Commit df93900

Browse files
committed
Update http-parser for CVE.
Motivation: http-parser shipped a patche for node.js CVE-2019-15605, which allowed HTTP request smuggling. This affected SwiftNIO as well, and so we need to immediately ship an update to help protect affected users. A CVE for SwiftNIO will follow, but as this patch is in the wild and SwiftNIO is known to be affected we should not delay shipping this fix. Modifications: - Update http-parser. - Add regression tests to validate this behaviour. Result: Close request smugging vector. (cherry picked from commit f94b22b)
1 parent 4409b57 commit df93900

7 files changed

Lines changed: 211 additions & 56 deletions

File tree

Sources/CNIOHTTPParser/c_nio_http_parser.c

Lines changed: 97 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,10 @@ enum header_states
384384
, h_transfer_encoding
385385
, h_upgrade
386386

387+
, h_matching_transfer_encoding_token_start
387388
, h_matching_transfer_encoding_chunked
389+
, h_matching_transfer_encoding_token
390+
388391
, h_matching_connection_token_start
389392
, h_matching_connection_keep_alive
390393
, h_matching_connection_close
@@ -1260,9 +1263,9 @@ size_t c_nio_http_parser_execute (http_parser *parser,
12601263

12611264
switch (parser->header_state) {
12621265
case h_general: {
1263-
size_t limit = data + len - p;
1264-
limit = MIN(limit, max_header_size);
1265-
while (p+1 < data + limit && TOKEN(p[1])) {
1266+
size_t left = data + len - p;
1267+
const char* pe = p + MIN(left, max_header_size);
1268+
while (p+1 < pe && TOKEN(p[1])) {
12661269
p++;
12671270
}
12681271
break;
@@ -1338,6 +1341,7 @@ size_t c_nio_http_parser_execute (http_parser *parser,
13381341
parser->header_state = h_general;
13391342
} else if (parser->index == sizeof(TRANSFER_ENCODING)-2) {
13401343
parser->header_state = h_transfer_encoding;
1344+
parser->flags |= F_TRANSFER_ENCODING;
13411345
}
13421346
break;
13431347

@@ -1419,10 +1423,14 @@ size_t c_nio_http_parser_execute (http_parser *parser,
14191423
if ('c' == c) {
14201424
parser->header_state = h_matching_transfer_encoding_chunked;
14211425
} else {
1422-
parser->header_state = h_general;
1426+
parser->header_state = h_matching_transfer_encoding_token;
14231427
}
14241428
break;
14251429

1430+
/* Multi-value `Transfer-Encoding` header */
1431+
case h_matching_transfer_encoding_token_start:
1432+
break;
1433+
14261434
case h_content_length:
14271435
if (UNLIKELY(!IS_NUM(ch))) {
14281436
SET_ERRNO(HPE_INVALID_CONTENT_LENGTH);
@@ -1499,28 +1507,25 @@ size_t c_nio_http_parser_execute (http_parser *parser,
14991507

15001508
switch (h_state) {
15011509
case h_general:
1502-
{
1503-
const char* p_cr;
1504-
const char* p_lf;
1505-
size_t limit = data + len - p;
1506-
1507-
limit = MIN(limit, max_header_size);
1508-
1509-
p_cr = (const char*) memchr(p, CR, limit);
1510-
p_lf = (const char*) memchr(p, LF, limit);
1511-
if (p_cr != NULL) {
1512-
if (p_lf != NULL && p_cr >= p_lf)
1513-
p = p_lf;
1514-
else
1515-
p = p_cr;
1516-
} else if (UNLIKELY(p_lf != NULL)) {
1517-
p = p_lf;
1518-
} else {
1519-
p = data + len;
1510+
{
1511+
size_t left = data + len - p;
1512+
const char* pe = p + MIN(left, max_header_size);
1513+
1514+
for (; p != pe; p++) {
1515+
ch = *p;
1516+
if (ch == CR || ch == LF) {
1517+
--p;
1518+
break;
1519+
}
1520+
if (!lenient && !IS_HEADER_CHAR(ch)) {
1521+
SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
1522+
goto error;
1523+
}
1524+
}
1525+
if (p == data + len)
1526+
--p;
1527+
break;
15201528
}
1521-
--p;
1522-
break;
1523-
}
15241529

15251530
case h_connection:
15261531
case h_transfer_encoding:
@@ -1569,16 +1574,41 @@ size_t c_nio_http_parser_execute (http_parser *parser,
15691574
goto error;
15701575

15711576
/* Transfer-Encoding: chunked */
1577+
case h_matching_transfer_encoding_token_start:
1578+
/* looking for 'Transfer-Encoding: chunked' */
1579+
if ('c' == c) {
1580+
h_state = h_matching_transfer_encoding_chunked;
1581+
} else if (STRICT_TOKEN(c)) {
1582+
/* TODO(indutny): similar code below does this, but why?
1583+
* At the very least it seems to be inconsistent given that
1584+
* h_matching_transfer_encoding_token does not check for
1585+
* `STRICT_TOKEN`
1586+
*/
1587+
h_state = h_matching_transfer_encoding_token;
1588+
} else if (c == ' ' || c == '\t') {
1589+
/* Skip lws */
1590+
} else {
1591+
h_state = h_general;
1592+
}
1593+
break;
1594+
15721595
case h_matching_transfer_encoding_chunked:
15731596
parser->index++;
15741597
if (parser->index > sizeof(CHUNKED)-1
15751598
|| c != CHUNKED[parser->index]) {
1576-
h_state = h_general;
1599+
h_state = h_matching_transfer_encoding_token;
15771600
} else if (parser->index == sizeof(CHUNKED)-2) {
15781601
h_state = h_transfer_encoding_chunked;
15791602
}
15801603
break;
15811604

1605+
case h_matching_transfer_encoding_token:
1606+
if (ch == ',') {
1607+
h_state = h_matching_transfer_encoding_token_start;
1608+
parser->index = 0;
1609+
}
1610+
break;
1611+
15821612
case h_matching_connection_token_start:
15831613
/* looking for 'Connection: keep-alive' */
15841614
if (c == 'k') {
@@ -1637,7 +1667,7 @@ size_t c_nio_http_parser_execute (http_parser *parser,
16371667
break;
16381668

16391669
case h_transfer_encoding_chunked:
1640-
if (ch != ' ') h_state = h_general;
1670+
if (ch != ' ') h_state = h_matching_transfer_encoding_token;
16411671
break;
16421672

16431673
case h_connection_keep_alive:
@@ -1771,12 +1801,17 @@ size_t c_nio_http_parser_execute (http_parser *parser,
17711801
REEXECUTE();
17721802
}
17731803

1774-
/* Cannot use chunked encoding and a content-length header together
1775-
per the HTTP specification. */
1776-
if ((parser->flags & F_CHUNKED) &&
1804+
/* Cannot us transfer-encoding and a content-length header together
1805+
per the HTTP specification. (RFC 7230 Section 3.3.3) */
1806+
if ((parser->flags & F_TRANSFER_ENCODING) &&
17771807
(parser->flags & F_CONTENTLENGTH)) {
1778-
SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
1779-
goto error;
1808+
/* Allow it for lenient parsing as long as `Transfer-Encoding` is
1809+
* not `chunked`
1810+
*/
1811+
if (!lenient || (parser->flags & F_CHUNKED)) {
1812+
SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
1813+
goto error;
1814+
}
17801815
}
17811816

17821817
UPDATE_STATE(s_headers_done);
@@ -1851,8 +1886,31 @@ size_t c_nio_http_parser_execute (http_parser *parser,
18511886
UPDATE_STATE(NEW_MESSAGE());
18521887
CALLBACK_NOTIFY(message_complete);
18531888
} else if (parser->flags & F_CHUNKED) {
1854-
/* chunked encoding - ignore Content-Length header */
1889+
/* chunked encoding - ignore Content-Length header,
1890+
* prepare for a chunk */
18551891
UPDATE_STATE(s_chunk_size_start);
1892+
} else if (parser->flags & F_TRANSFER_ENCODING) {
1893+
if (parser->type == HTTP_REQUEST && !lenient) {
1894+
/* RFC 7230 3.3.3 */
1895+
1896+
/* If a Transfer-Encoding header field
1897+
* is present in a request and the chunked transfer coding is not
1898+
* the final encoding, the message body length cannot be determined
1899+
* reliably; the server MUST respond with the 400 (Bad Request)
1900+
* status code and then close the connection.
1901+
*/
1902+
SET_ERRNO(HPE_INVALID_TRANSFER_ENCODING);
1903+
RETURN(p - data); /* Error */
1904+
} else {
1905+
/* RFC 7230 3.3.3 */
1906+
1907+
/* If a Transfer-Encoding header field is present in a response and
1908+
* the chunked transfer coding is not the final encoding, the
1909+
* message body length is determined by reading the connection until
1910+
* it is closed by the server.
1911+
*/
1912+
UPDATE_STATE(s_body_identity_eof);
1913+
}
18561914
} else {
18571915
if (parser->content_length == 0) {
18581916
/* Content-Length header given but zero: Content-Length: 0\r\n */
@@ -2106,6 +2164,12 @@ c_nio_http_message_needs_eof (const http_parser *parser)
21062164
return 0;
21072165
}
21082166

2167+
/* RFC 7230 3.3.3, see `s_headers_almost_done` */
2168+
if ((parser->flags & F_TRANSFER_ENCODING) &&
2169+
(parser->flags & F_CHUNKED) == 0) {
2170+
return 1;
2171+
}
2172+
21092173
if ((parser->flags & F_CHUNKED) || parser->content_length != ULLONG_MAX) {
21102174
return 0;
21112175
}

Sources/CNIOHTTPParser/include/c_nio_http_parser.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ extern "C" {
3030
/* Also update SONAME in the Makefile whenever you change these. */
3131
#define HTTP_PARSER_VERSION_MAJOR 2
3232
#define HTTP_PARSER_VERSION_MINOR 9
33-
#define HTTP_PARSER_VERSION_PATCH 0
33+
#define HTTP_PARSER_VERSION_PATCH 3
3434

3535
#include <stddef.h>
3636
#if defined(_WIN32) && !defined(__MINGW32__) && \
@@ -228,6 +228,7 @@ enum flags
228228
, F_UPGRADE = 1 << 5
229229
, F_SKIPBODY = 1 << 6
230230
, F_CONTENTLENGTH = 1 << 7
231+
, F_TRANSFER_ENCODING = 1 << 8
231232
};
232233

233234

@@ -274,6 +275,8 @@ enum flags
274275
"unexpected content-length header") \
275276
XX(INVALID_CHUNK_SIZE, \
276277
"invalid character in chunk size header") \
278+
XX(INVALID_TRANSFER_ENCODING, \
279+
"request has invalid transfer-encoding") \
277280
XX(INVALID_CONSTANT, "invalid constant string") \
278281
XX(INVALID_INTERNAL_STATE, "encountered unexpected internal state")\
279282
XX(STRICT, "strict mode assertion failed") \
@@ -296,11 +299,11 @@ enum http_errno {
296299
struct http_parser {
297300
/** PRIVATE **/
298301
unsigned int type : 2; /* enum http_parser_type */
299-
unsigned int flags : 8; /* F_* values from 'flags' enum; semi-public */
300302
unsigned int state : 7; /* enum state from http_parser.c */
301303
unsigned int header_state : 7; /* enum header_state from http_parser.c */
302304
unsigned int index : 7; /* index into current matcher */
303305
unsigned int lenient_http_headers : 1;
306+
unsigned int flags : 16; /* F_* values from 'flags' enum; semi-public */
304307

305308
uint32_t nread; /* # bytes read in various scenarios */
306309
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */

Sources/NIOHTTP1/HTTPDecoder.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,9 @@ extension HTTPParserError {
706706
return .paused
707707
case HPE_UNKNOWN:
708708
return .unknown
709+
case HPE_INVALID_TRANSFER_ENCODING:
710+
// The downside of enums here, we don't have a case for this. Map it to .unknown for now.
711+
return .unknown
709712
default:
710713
return nil
711714
}

Tests/NIOHTTP1Tests/HTTPDecoderLengthTest+XCTest.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension HTTPDecoderLengthTest {
4646
("testIgnoresTransferEncodingFieldOn304Responses", testIgnoresTransferEncodingFieldOn304Responses),
4747
("testIgnoresContentLengthFieldOn304Responses", testIgnoresContentLengthFieldOn304Responses),
4848
("testEarlyFinishWithoutLengthAtAllOn304Responses", testEarlyFinishWithoutLengthAtAllOn304Responses),
49-
("testMultipleTEWithChunkedLastHasNoBodyOnRequest", testMultipleTEWithChunkedLastHasNoBodyOnRequest),
49+
("testMultipleTEWithChunkedLastWorksFine", testMultipleTEWithChunkedLastWorksFine),
5050
("testMultipleTEWithChunkedFirstHasNoBodyOnRequest", testMultipleTEWithChunkedFirstHasNoBodyOnRequest),
5151
("testMultipleTEWithChunkedInTheMiddleHasNoBodyOnRequest", testMultipleTEWithChunkedInTheMiddleHasNoBodyOnRequest),
5252
("testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithChannelInactive", testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithChannelInactive),

Tests/NIOHTTP1Tests/HTTPDecoderLengthTest.swift

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -292,14 +292,26 @@ class HTTPDecoderLengthTest: XCTestCase {
292292
try assertIgnoresLengthFields(requestMethod: .GET, responseStatus: .notModified, responseFramingField: .neither)
293293
}
294294

295-
private func assertRequestTransferEncodingHasNoBody(transferEncodingHeader: String) throws {
295+
private func assertRequestTransferEncodingInError(transferEncodingHeader: String) throws {
296296
XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait())
297297

298298
let handler = MessageEndHandler<HTTPRequestHead, ByteBuffer>()
299299
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
300300

301301
// Send a GET with the appropriate Transfer Encoding header.
302-
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n")))
302+
XCTAssertThrowsError(try channel.writeInbound(ByteBuffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n"))) { error in
303+
XCTAssertEqual(error as? HTTPParserError, .unknown)
304+
}
305+
}
306+
307+
func testMultipleTEWithChunkedLastWorksFine() throws {
308+
XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait())
309+
310+
let handler = MessageEndHandler<HTTPRequestHead, ByteBuffer>()
311+
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
312+
313+
// Send a GET with the appropriate Transfer Encoding header.
314+
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: "POST / HTTP/1.1\r\nTransfer-Encoding: gzip, chunked\r\n\r\n0\r\n\r\n")))
303315

304316
// We should have a request, no body, and immediately see end of request.
305317
XCTAssert(handler.seenHead)
@@ -309,22 +321,12 @@ class HTTPDecoderLengthTest: XCTestCase {
309321
XCTAssertTrue(try channel.finish().isClean)
310322
}
311323

312-
func testMultipleTEWithChunkedLastHasNoBodyOnRequest() throws {
313-
// This is not quite right: RFC 7230 should allow this as chunked. However, http_parser
314-
// does not, so we don't either.
315-
try assertRequestTransferEncodingHasNoBody(transferEncodingHeader: "gzip, chunked")
316-
}
317-
318324
func testMultipleTEWithChunkedFirstHasNoBodyOnRequest() throws {
319-
// Here http_parser is again wrong: strictly this should 400. Again, though,
320-
// if http_parser doesn't support this neither do we.
321-
try assertRequestTransferEncodingHasNoBody(transferEncodingHeader: "chunked, gzip")
325+
try assertRequestTransferEncodingInError(transferEncodingHeader: "chunked, gzip")
322326
}
323327

324328
func testMultipleTEWithChunkedInTheMiddleHasNoBodyOnRequest() throws {
325-
// Here http_parser is again wrong: strictly this should 400. Again, though,
326-
// if http_parser doesn't support this neither do we.
327-
try assertRequestTransferEncodingHasNoBody(transferEncodingHeader: "gzip, chunked, deflate")
329+
try assertRequestTransferEncodingInError(transferEncodingHeader: "gzip, chunked, deflate")
328330
}
329331

330332
private func assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws {
@@ -366,10 +368,51 @@ class HTTPDecoderLengthTest: XCTestCase {
366368
XCTAssertTrue(try channel.finish().isClean)
367369
}
368370

371+
private func assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: String, eofMechanism: EOFMechanism) throws {
372+
XCTAssertNoThrow(try channel.pipeline.addHandler(HTTPRequestEncoder()).wait())
373+
XCTAssertNoThrow(try channel.pipeline.addHandler(ByteToMessageHandler(HTTPResponseDecoder())).wait())
374+
375+
let handler = MessageEndHandler<HTTPResponseHead, ByteBuffer>()
376+
XCTAssertNoThrow(try channel.pipeline.addHandler(handler).wait())
377+
378+
// Prime the decoder with a request and consume it.
379+
XCTAssertTrue(try channel.writeOutbound(HTTPClientRequestPart.head(HTTPRequestHead(version: .init(major: 1, minor: 1),
380+
method: .GET,
381+
uri: "/"))).isFull)
382+
XCTAssertNoThrow(XCTAssertNotNil(try channel.readOutbound(as: ByteBuffer.self)))
383+
384+
// Send a 200 with the appropriate Transfer Encoding header. We should see the request.
385+
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: "HTTP/1.1 200 OK\r\nTransfer-Encoding: \(transferEncodingHeader)\r\n\r\n")))
386+
XCTAssert(handler.seenHead)
387+
XCTAssertFalse(handler.seenBody)
388+
XCTAssertFalse(handler.seenEnd)
389+
390+
// Now send body. Note that this *is* chunk encoded. We should also see a body.
391+
XCTAssertNoThrow(try channel.writeInbound(ByteBuffer(string: "9\r\ncaribbean\r\n")))
392+
XCTAssert(handler.seenHead)
393+
XCTAssert(handler.seenBody)
394+
XCTAssertFalse(handler.seenEnd)
395+
396+
// Now send EOF. This should error, as we're expecting the end chunk.
397+
if case .halfClosure = eofMechanism {
398+
channel.pipeline.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
399+
} else {
400+
channel.pipeline.fireChannelInactive()
401+
}
402+
403+
XCTAssert(handler.seenHead)
404+
XCTAssert(handler.seenBody)
405+
XCTAssertFalse(handler.seenEnd)
406+
407+
XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in
408+
XCTAssertEqual(error as? HTTPParserError, .invalidEOFState)
409+
}
410+
411+
XCTAssertTrue(try channel.finish().isClean)
412+
}
413+
369414
func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithChannelInactive() throws {
370-
// This is not right: RFC 7230 should allow this as chunked, but http_parser instead parses it as
371-
// EOF-terminated. We can't easily override that, so we don't.
372-
try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked", eofMechanism: .channelInactive)
415+
try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .channelInactive)
373416
}
374417

375418
func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithChannelInactive() throws {
@@ -383,9 +426,7 @@ class HTTPDecoderLengthTest: XCTestCase {
383426
}
384427

385428
func testMultipleTEWithChunkedLastHasEOFBodyOnResponseWithHalfClosure() throws {
386-
// This is not right: RFC 7230 should allow this as chunked, but http_parser instead parses it as
387-
// EOF-terminated. We can't easily override that, so we don't.
388-
try assertResponseTransferEncodingHasBodyTerminatedByEOF(transferEncodingHeader: "gzip, chunked", eofMechanism: .halfClosure)
429+
try assertResponseTransferEncodingHasBodyTerminatedByEndOfChunk(transferEncodingHeader: "gzip, chunked", eofMechanism: .halfClosure)
389430
}
390431

391432
func testMultipleTEWithChunkedFirstHasEOFBodyOnResponseWithHalfClosure() throws {

0 commit comments

Comments
 (0)