Skip to content

Commit

Permalink
deps: update http-parser to version 1.1
Browse files Browse the repository at this point in the history
includes parsing improvements to ensure closer HTTP spec conformance

PR-URL: nodejs-private/node-private#22
  • Loading branch information
jasnell committed Feb 9, 2016
1 parent 85e1d9f commit 375f355
Show file tree
Hide file tree
Showing 12 changed files with 393 additions and 8 deletions.
29 changes: 27 additions & 2 deletions deps/http_parser/http_parser.c
Expand Up @@ -387,6 +387,8 @@ enum http_host_state
(IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_')
#endif

#define IS_HEADER_CHAR(ch) \
(ch == CR || ch == LF || ch == 9 || (ch > 31 && ch != 127))

#define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res)

Expand Down Expand Up @@ -590,6 +592,8 @@ size_t http_parser_execute (http_parser *parser,
const char *url_mark = 0;
const char *body_mark = 0;

const unsigned char lenient = parser->lenient_http_headers;

/* We're in an error state. Don't bother doing anything. */
if (HTTP_PARSER_ERRNO(parser) != HPE_OK) {
return 0;
Expand Down Expand Up @@ -1311,7 +1315,12 @@ size_t http_parser_execute (http_parser *parser,
|| c != CONTENT_LENGTH[parser->index]) {
parser->header_state = h_general;
} else if (parser->index == sizeof(CONTENT_LENGTH)-2) {
parser->header_state = h_content_length;
if (parser->flags & F_CONTENTLENGTH) {
SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
goto error;
}
parser->header_state = h_content_length;
parser->flags |= F_CONTENTLENGTH;
}
break;

Expand Down Expand Up @@ -1457,6 +1466,11 @@ size_t http_parser_execute (http_parser *parser,
goto reexecute_byte;
}

if (!lenient && !IS_HEADER_CHAR(ch)) {
SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
goto error;
}

c = LOWER(ch);

switch (parser->header_state) {
Expand Down Expand Up @@ -1541,7 +1555,10 @@ size_t http_parser_execute (http_parser *parser,

case s_header_almost_done:
{
STRICT_CHECK(ch != LF);
if (ch != LF) {
SET_ERRNO(HPE_LF_EXPECTED);
goto error;
}

parser->state = s_header_value_lws;

Expand Down Expand Up @@ -1585,6 +1602,14 @@ size_t http_parser_execute (http_parser *parser,
break;
}

/* Cannot use chunked encoding and a content-length header together
per the HTTP specification. */
if ((parser->flags & F_CHUNKED) &&
(parser->flags & F_CONTENTLENGTH)) {
SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
goto error;
}

parser->state = s_headers_done;

/* Set this here so that on_headers_complete() callbacks can see it */
Expand Down
12 changes: 8 additions & 4 deletions deps/http_parser/http_parser.h
Expand Up @@ -25,7 +25,7 @@ extern "C" {
#endif

#define HTTP_PARSER_VERSION_MAJOR 1
#define HTTP_PARSER_VERSION_MINOR 0
#define HTTP_PARSER_VERSION_MINOR 1

#include <sys/types.h>
#if defined(_WIN32) && !defined(__MINGW32__) && (!defined(_MSC_VER) || _MSC_VER<1600)
Expand Down Expand Up @@ -137,6 +137,7 @@ enum flags
, F_TRAILING = 1 << 3
, F_UPGRADE = 1 << 4
, F_SKIPBODY = 1 << 5
, F_CONTENTLENGTH = 1 << 6
};


Expand Down Expand Up @@ -176,6 +177,8 @@ enum flags
XX(INVALID_HEADER_TOKEN, "invalid character in header") \
XX(INVALID_CONTENT_LENGTH, \
"invalid character in content-length header") \
XX(UNEXPECTED_CONTENT_LENGTH, \
"unexpected content-length header") \
XX(INVALID_CHUNK_SIZE, \
"invalid character in chunk size header") \
XX(INVALID_CONSTANT, "invalid constant string") \
Expand Down Expand Up @@ -207,10 +210,11 @@ enum http_errno {
struct http_parser {
/** PRIVATE **/
unsigned char type : 2; /* enum http_parser_type */
unsigned char flags : 6; /* F_* values from 'flags' enum; semi-public */
unsigned char flags : 7; /* F_* values from 'flags' enum; semi-public */
unsigned char state; /* enum state from http_parser.c */
unsigned char header_state; /* enum header_state from http_parser.c */
unsigned char index; /* index into current matcher */
unsigned char header_state : 7; /* enum header_state from http_parser.c */
unsigned char index : 7; /* index into current matcher */
unsigned char lenient_http_headers : 1;

uint32_t nread; /* # bytes read in various scenarios */
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */
Expand Down
163 changes: 163 additions & 0 deletions deps/http_parser/test.c
Expand Up @@ -2651,6 +2651,156 @@ test_simple (const char *buf, enum http_errno err_expected)
}
}

void
test_invalid_header_content (int req, const char* str)
{
http_parser parser;
http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE);
size_t parsed;
const char *buf;
buf = req ?
"GET / HTTP/1.1\r\n" :
"HTTP/1.1 200 OK\r\n";
parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf));
assert(parsed == strlen(buf));

buf = str;
size_t buflen = strlen(buf);

parsed = http_parser_execute(&parser, &settings_null, buf, buflen);
if (parsed != buflen) {
assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN);
return;
}

fprintf(stderr,
"\n*** Error expected but none in invalid header content test ***\n");
abort();
}

void
test_invalid_header_field_content_error (int req)
{
test_invalid_header_content(req, "Foo: F\01ailure");
test_invalid_header_content(req, "Foo: B\02ar");
}

void
test_invalid_header_field (int req, const char* str)
{
http_parser parser;
http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE);
size_t parsed;
const char *buf;
buf = req ?
"GET / HTTP/1.1\r\n" :
"HTTP/1.1 200 OK\r\n";
parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf));
assert(parsed == strlen(buf));

buf = str;
size_t buflen = strlen(buf);

parsed = http_parser_execute(&parser, &settings_null, buf, buflen);
if (parsed != buflen) {
assert(HTTP_PARSER_ERRNO(&parser) == HPE_INVALID_HEADER_TOKEN);
return;
}

fprintf(stderr,
"\n*** Error expected but none in invalid header token test ***\n");
abort();
}

void
test_invalid_header_field_token_error (int req)
{
test_invalid_header_field(req, "Fo@: Failure");
test_invalid_header_field(req, "Foo\01\test: Bar");
}

void
test_double_content_length_error (int req)
{
http_parser parser;
http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE);
size_t parsed;
const char *buf;
buf = req ?
"GET / HTTP/1.1\r\n" :
"HTTP/1.1 200 OK\r\n";
parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf));
assert(parsed == strlen(buf));

buf = "Content-Length: 0\r\nContent-Length: 1\r\n\r\n";
size_t buflen = strlen(buf);

parsed = http_parser_execute(&parser, &settings_null, buf, buflen);
if (parsed != buflen) {
assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH);
return;
}

fprintf(stderr,
"\n*** Error expected but none in double content-length test ***\n");
abort();
}

void
test_chunked_content_length_error (int req)
{
http_parser parser;
http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE);
size_t parsed;
const char *buf;
buf = req ?
"GET / HTTP/1.1\r\n" :
"HTTP/1.1 200 OK\r\n";
parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf));
assert(parsed == strlen(buf));

buf = "Transfer-Encoding: chunked\r\nContent-Length: 1\r\n\r\n";
size_t buflen = strlen(buf);

parsed = http_parser_execute(&parser, &settings_null, buf, buflen);
if (parsed != buflen) {
assert(HTTP_PARSER_ERRNO(&parser) == HPE_UNEXPECTED_CONTENT_LENGTH);
return;
}

fprintf(stderr,
"\n*** Error expected but none in chunked content-length test ***\n");
abort();
}

void
test_header_cr_no_lf_error (int req)
{
http_parser parser;
http_parser_init(&parser, req ? HTTP_REQUEST : HTTP_RESPONSE);
size_t parsed;
const char *buf;
buf = req ?
"GET / HTTP/1.1\r\n" :
"HTTP/1.1 200 OK\r\n";
parsed = http_parser_execute(&parser, &settings_null, buf, strlen(buf));
assert(parsed == strlen(buf));

buf = "Foo: 1\rBar: 1\r\n\r\n";
size_t buflen = strlen(buf);

parsed = http_parser_execute(&parser, &settings_null, buf, buflen);
if (parsed != buflen) {
assert(HTTP_PARSER_ERRNO(&parser) == HPE_LF_EXPECTED);
return;
}

fprintf(stderr,
"\n*** Error expected but none in header whitespace test ***\n");
abort();
}


void
test_header_overflow_error (int req)
{
Expand Down Expand Up @@ -3048,6 +3198,19 @@ main (void)
test_header_content_length_overflow_error();
test_chunk_content_length_overflow_error();

//// HEADER FIELD CONDITIONS
test_double_content_length_error(HTTP_REQUEST);
test_chunked_content_length_error(HTTP_REQUEST);
test_header_cr_no_lf_error(HTTP_REQUEST);
test_invalid_header_field_token_error(HTTP_REQUEST);
test_invalid_header_field_content_error(HTTP_REQUEST);
test_double_content_length_error(HTTP_RESPONSE);
test_chunked_content_length_error(HTTP_RESPONSE);
test_header_cr_no_lf_error(HTTP_RESPONSE);
test_invalid_header_field_token_error(HTTP_RESPONSE);
test_invalid_header_field_content_error(HTTP_RESPONSE);


//// RESPONSES

for (i = 0; i < response_count; i++) {
Expand Down
3 changes: 3 additions & 0 deletions src/node_http_parser.cc
Expand Up @@ -24,6 +24,7 @@
#include "v8.h"
#include "node.h"
#include "node_buffer.h"
#include "node_revert.h"

#include <string.h> /* strdup() */
#if !defined(_MSC_VER)
Expand Down Expand Up @@ -546,6 +547,8 @@ class Parser : public ObjectWrap {

void Init(enum http_parser_type type) {
http_parser_init(&parser_, type);
/* Allow the strict http header parsing to be reverted */
parser_.lenient_http_headers = IsReverted(REVERT_CVE_2016_2216) ? 1 : 0;
url_.Reset();
num_fields_ = 0;
num_values_ = 0;
Expand Down
4 changes: 2 additions & 2 deletions src/node_revert.h
Expand Up @@ -32,8 +32,8 @@
* For *master* this list should always be empty!
*
**/
#define REVERSIONS(XX)
// XX(CVE_2016_PEND, "CVE-2016-PEND", "Vulnerability Title")
#define REVERSIONS(XX) \
XX(CVE_2016_2216, "CVE-2016-2216", "Strict HTTP Header Parsing")

namespace node {

Expand Down
26 changes: 26 additions & 0 deletions test/simple/test-http-client-reject-chunked-with-content-length.js
@@ -0,0 +1,26 @@
var common = require('../common');
var http = require('http');
var net = require('net');
var assert = require('assert');

var reqstr = 'HTTP/1.1 200 OK\r\n' +
'Content-Length: 1\r\n' +
'Transfer-Encoding: chunked\r\n\r\n';

var server = net.createServer(function(socket) {
socket.write(reqstr);
});

server.listen(common.PORT, function() {
// The callback should not be called because the server is sending
// both a Content-Length header and a Transfer-Encoding: chunked
// header, which is a violation of the HTTP spec.
var req = http.get({port:common.PORT}, function(res) {
assert.fail(null, null, 'callback should not be called');
});
req.on('error', common.mustCall(function(err) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH');
server.close();
}));
});
25 changes: 25 additions & 0 deletions test/simple/test-http-client-reject-cr-no-lf.js
@@ -0,0 +1,25 @@
var common = require('../common');
var http = require('http');
var net = require('net');
var assert = require('assert');

var reqstr = 'HTTP/1.1 200 OK\r\n' +
'Foo: Bar\r' +
'Content-Length: 1\r\n\r\n';

var server = net.createServer(function(socket) {
socket.write(reqstr);
});

server.listen(common.PORT, function() {
// The callback should not be called because the server is sending a
// header field that ends only in \r with no following \n
var req = http.get({port:common.PORT}, function(res) {
assert.fail(null, null, 'callback should not be called');
});
req.on('error', common.mustCall(function(err) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_LF_EXPECTED');
server.close();
}));
});

0 comments on commit 375f355

Please sign in to comment.