Browse Source

deps: update http-parser to version 2.3.1

includes parsing improvements to ensure closer HTTP spec conformance

PR-URL: https://github.com/nodejs/node-private/pull/21
v0.12-staging
James M Snell 9 years ago
parent
commit
d86fc39666
  1. 2
      deps/http_parser/Makefile
  2. 31
      deps/http_parser/http_parser.c
  3. 12
      deps/http_parser/http_parser.h
  4. 162
      deps/http_parser/test.c
  5. 3
      src/node_http_parser.cc
  6. 4
      src/node_revert.h
  7. 26
      test/simple/test-http-client-reject-chunked-with-content-length.js
  8. 25
      test/simple/test-http-client-reject-cr-no-lf.js
  9. 31
      test/simple/test-http-double-content-length.js
  10. 2
      test/simple/test-http-header-response-splitting.js
  11. 50
      test/simple/test-http-response-multi-content-length.js
  12. 6
      test/simple/test-http-server-multiheaders2.js
  13. 29
      test/simple/test-http-server-reject-chunked-with-content-length.js
  14. 30
      test/simple/test-http-server-reject-cr-no-lf.js

2
deps/http_parser/Makefile

@ -19,7 +19,7 @@
# IN THE SOFTWARE. # IN THE SOFTWARE.
PLATFORM ?= $(shell sh -c 'uname -s | tr "[A-Z]" "[a-z]"') PLATFORM ?= $(shell sh -c 'uname -s | tr "[A-Z]" "[a-z]"')
SONAME ?= libhttp_parser.so.2.3 SONAME ?= libhttp_parser.so.2.3.1
CC?=gcc CC?=gcc
AR?=ar AR?=ar

31
deps/http_parser/http_parser.c

@ -383,6 +383,12 @@ enum http_host_state
(IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_') (IS_ALPHANUM(c) || (c) == '.' || (c) == '-' || (c) == '_')
#endif #endif
/**
* Verify that a char is a valid visible (printable) US-ASCII
* character or %x80-FF
**/
#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) #define start_state (parser->type == HTTP_REQUEST ? s_start_req : s_start_res)
@ -587,6 +593,8 @@ size_t http_parser_execute (http_parser *parser,
const char *body_mark = 0; const char *body_mark = 0;
const char *status_mark = 0; const char *status_mark = 0;
const unsigned int lenient = parser->lenient_http_headers;
/* We're in an error state. Don't bother doing anything. */ /* We're in an error state. Don't bother doing anything. */
if (HTTP_PARSER_ERRNO(parser) != HPE_OK) { if (HTTP_PARSER_ERRNO(parser) != HPE_OK) {
return 0; return 0;
@ -1340,7 +1348,12 @@ size_t http_parser_execute (http_parser *parser,
|| c != CONTENT_LENGTH[parser->index]) { || c != CONTENT_LENGTH[parser->index]) {
parser->header_state = h_general; parser->header_state = h_general;
} else if (parser->index == sizeof(CONTENT_LENGTH)-2) { } else if (parser->index == sizeof(CONTENT_LENGTH)-2) {
if (parser->flags & F_CONTENTLENGTH) {
SET_ERRNO(HPE_UNEXPECTED_CONTENT_LENGTH);
goto error;
}
parser->header_state = h_content_length; parser->header_state = h_content_length;
parser->flags |= F_CONTENTLENGTH;
} }
break; break;
@ -1486,6 +1499,11 @@ size_t http_parser_execute (http_parser *parser,
goto reexecute_byte; goto reexecute_byte;
} }
if (!lenient && !IS_HEADER_CHAR(ch)) {
SET_ERRNO(HPE_INVALID_HEADER_TOKEN);
goto error;
}
c = LOWER(ch); c = LOWER(ch);
switch (parser->header_state) { switch (parser->header_state) {
@ -1570,7 +1588,10 @@ size_t http_parser_execute (http_parser *parser,
case s_header_almost_done: 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; parser->state = s_header_value_lws;
break; break;
@ -1634,6 +1655,14 @@ size_t http_parser_execute (http_parser *parser,
break; 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; parser->state = s_headers_done;
/* Set this here so that on_headers_complete() callbacks can see it */ /* Set this here so that on_headers_complete() callbacks can see it */

12
deps/http_parser/http_parser.h

@ -27,7 +27,7 @@ extern "C" {
/* Also update SONAME in the Makefile whenever you change these. */ /* Also update SONAME in the Makefile whenever you change these. */
#define HTTP_PARSER_VERSION_MAJOR 2 #define HTTP_PARSER_VERSION_MAJOR 2
#define HTTP_PARSER_VERSION_MINOR 3 #define HTTP_PARSER_VERSION_MINOR 3
#define HTTP_PARSER_VERSION_PATCH 0 #define HTTP_PARSER_VERSION_PATCH 1
#include <sys/types.h> #include <sys/types.h>
#if defined(_WIN32) && !defined(__MINGW32__) && (!defined(_MSC_VER) || _MSC_VER<1600) #if defined(_WIN32) && !defined(__MINGW32__) && (!defined(_MSC_VER) || _MSC_VER<1600)
@ -130,6 +130,7 @@ enum flags
, F_TRAILING = 1 << 3 , F_TRAILING = 1 << 3
, F_UPGRADE = 1 << 4 , F_UPGRADE = 1 << 4
, F_SKIPBODY = 1 << 5 , F_SKIPBODY = 1 << 5
, F_CONTENTLENGTH = 1 << 6
}; };
@ -170,6 +171,8 @@ enum flags
XX(INVALID_HEADER_TOKEN, "invalid character in header") \ XX(INVALID_HEADER_TOKEN, "invalid character in header") \
XX(INVALID_CONTENT_LENGTH, \ XX(INVALID_CONTENT_LENGTH, \
"invalid character in content-length header") \ "invalid character in content-length header") \
XX(UNEXPECTED_CONTENT_LENGTH, \
"unexpected content-length header") \
XX(INVALID_CHUNK_SIZE, \ XX(INVALID_CHUNK_SIZE, \
"invalid character in chunk size header") \ "invalid character in chunk size header") \
XX(INVALID_CONSTANT, "invalid constant string") \ XX(INVALID_CONSTANT, "invalid constant string") \
@ -194,10 +197,11 @@ enum http_errno {
struct http_parser { struct http_parser {
/** PRIVATE **/ /** PRIVATE **/
unsigned int type : 2; /* enum http_parser_type */ unsigned int type : 2; /* enum http_parser_type */
unsigned int flags : 6; /* F_* values from 'flags' enum; semi-public */ unsigned int flags : 7; /* F_* values from 'flags' enum; semi-public */
unsigned int state : 8; /* enum state from http_parser.c */ unsigned int state : 8; /* enum state from http_parser.c */
unsigned int header_state : 8; /* enum header_state from http_parser.c */ unsigned int header_state : 7; /* enum header_state from http_parser.c */
unsigned int index : 8; /* index into current matcher */ unsigned int index : 7; /* index into current matcher */
unsigned int lenient_http_headers : 1;
uint32_t nread; /* # bytes read in various scenarios */ uint32_t nread; /* # bytes read in various scenarios */
uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */ uint64_t content_length; /* # bytes in body (0 if no Content-Length header) */

162
deps/http_parser/test.c

@ -2936,6 +2936,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 void
test_header_overflow_error (int req) test_header_overflow_error (int req)
{ {
@ -3343,6 +3493,18 @@ main (void)
test_header_content_length_overflow_error(); test_header_content_length_overflow_error();
test_chunk_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 //// RESPONSES
for (i = 0; i < response_count; i++) { for (i = 0; i < response_count; i++) {

3
src/node_http_parser.cc

@ -22,6 +22,7 @@
#include "node.h" #include "node.h"
#include "node_buffer.h" #include "node_buffer.h"
#include "node_http_parser.h" #include "node_http_parser.h"
#include "node_revert.h"
#include "base-object.h" #include "base-object.h"
#include "base-object-inl.h" #include "base-object-inl.h"
@ -533,6 +534,8 @@ class Parser : public BaseObject {
void Init(enum http_parser_type type) { void Init(enum http_parser_type type) {
http_parser_init(&parser_, 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(); url_.Reset();
status_message_.Reset(); status_message_.Reset();
num_fields_ = 0; num_fields_ = 0;

4
src/node_revert.h

@ -32,8 +32,8 @@
* For *master* this list should always be empty! * For *master* this list should always be empty!
* *
**/ **/
#define REVERSIONS(XX) #define REVERSIONS(XX) \
// XX(CVE_2016_PEND, "CVE-2016-PEND", "Vulnerability Title") XX(CVE_2016_2216, "CVE-2016-2216", "Strict HTTP Header Parsing")
namespace node { namespace node {

26
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
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();
}));
});

31
test/simple/test-http-double-content-length.js

@ -0,0 +1,31 @@
var common = require('../common');
var http = require('http');
var assert = require('assert');
// The callback should never be invoked because the server
// should respond with a 400 Client Error when a double
// Content-Length header is received.
var server = http.createServer(function(req, res) {
assert(false, 'callback should not have been invoked');
res.end();
});
server.on('clientError', common.mustCall(function(err, socket) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH');
socket.destroy();
}));
server.listen(common.PORT, function() {
var req = http.get({
port: common.PORT,
// Send two content-length header values.
headers: {'Content-Length': [1, 2]}},
function(res) {
assert.fail(null, null, 'an error should have occurred');
server.close();
}
);
req.on('error', common.mustCall(function() {
server.close();
}));
});

2
test/simple/test-http-header-response-splitting.js

@ -18,7 +18,7 @@
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE. // USE OR OTHER DEALINGS IN THE SOFTWARE.
// Flags: --security-revert=CVE-2016-2216
var common = require('../common'), var common = require('../common'),
assert = require('assert'), assert = require('assert'),
http = require('http'); http = require('http');

50
test/simple/test-http-response-multi-content-length.js

@ -0,0 +1,50 @@
var common = require('../common');
var http = require('http');
var assert = require('assert');
var MAX_COUNT = 2;
var server = http.createServer(function(req, res) {
var num = req.headers['x-num'];
// TODO(@jasnell) At some point this should be refactored as the API
// should not be allowing users to set multiple content-length values
// in the first place.
switch (num) {
case '1':
res.setHeader('content-length', [2, 1]);
break;
case '2':
res.writeHead(200, {'content-length': [1, 2]});
break;
default:
assert.fail(null, null, 'should never get here');
}
res.end('ok');
});
var count = 0;
server.listen(common.PORT, common.mustCall(function() {
for (var n = 1; n <= MAX_COUNT ; n++) {
// This runs twice, the first time, the server will use
// setHeader, the second time it uses writeHead. In either
// case, the error handler must be called because the client
// is not allowed to accept multiple content-length headers.
http.get(
{port:common.PORT, headers:{'x-num': n}},
function(res) {
assert(false, 'client allowed multiple content-length headers.');
}
).on('error', common.mustCall(function(err) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH');
count++;
if (count === MAX_COUNT)
server.close();
}));
}
}));
process.on('exit', function() {
assert.equal(count, MAX_COUNT);
});

6
test/simple/test-http-server-multiheaders2.js

@ -75,7 +75,6 @@ var srv = http.createServer(function(req, res) {
multipleAllowed.forEach(function(header) { multipleAllowed.forEach(function(header) {
assert.equal(req.headers[header.toLowerCase()], 'foo, bar', 'header parsed incorrectly: ' + header); assert.equal(req.headers[header.toLowerCase()], 'foo, bar', 'header parsed incorrectly: ' + header);
}); });
assert.equal(req.headers['content-length'], 0);
res.writeHead(200, {'Content-Type' : 'text/plain'}); res.writeHead(200, {'Content-Type' : 'text/plain'});
res.end('EOF'); res.end('EOF');
@ -93,10 +92,7 @@ var headers = []
.concat(multipleAllowed.map(makeHeader('foo'))) .concat(multipleAllowed.map(makeHeader('foo')))
.concat(multipleForbidden.map(makeHeader('foo'))) .concat(multipleForbidden.map(makeHeader('foo')))
.concat(multipleAllowed.map(makeHeader('bar'))) .concat(multipleAllowed.map(makeHeader('bar')))
.concat(multipleForbidden.map(makeHeader('bar'))) .concat(multipleForbidden.map(makeHeader('bar')));
// content-length is a special case since node.js
// is dropping connetions with non-numeric headers
.concat([['content-length', 0], ['content-length', 123]]);
srv.listen(common.PORT, function() { srv.listen(common.PORT, function() {
http.get({ http.get({

29
test/simple/test-http-server-reject-chunked-with-content-length.js

@ -0,0 +1,29 @@
var common = require('../common');
var http = require('http');
var net = require('net');
var assert = require('assert');
var reqstr = 'POST / HTTP/1.1\r\n' +
'Content-Length: 1\r\n' +
'Transfer-Encoding: chunked\r\n\r\n';
var server = http.createServer(function(req, res) {
assert.fail(null, null, 'callback should not be invoked');
});
server.on('clientError', common.mustCall(function(err) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_UNEXPECTED_CONTENT_LENGTH');
server.close();
}));
server.listen(common.PORT, function() {
var client = net.connect({port: common.PORT}, function() {
client.write(reqstr);
client.end();
});
client.on('data', function(data) {
// Should not get to this point because the server should simply
// close the connection without returning any data.
assert.fail(null, null, 'no data should be returned by the server');
});
client.on('end', common.mustCall(function() {}));
});

30
test/simple/test-http-server-reject-cr-no-lf.js

@ -0,0 +1,30 @@
var common = require('../common');
var net = require('net');
var http = require('http');
var assert = require('assert');
var str = 'GET / HTTP/1.1\r\n' +
'Dummy: Header\r' +
'Content-Length: 1\r\n' +
'\r\n';
var server = http.createServer(function(req, res) {
assert.fail(null, null, 'this should not be called');
});
server.on('clientError', common.mustCall(function(err) {
assert(/^Parse Error/.test(err.message));
assert.equal(err.code, 'HPE_LF_EXPECTED');
server.close();
}));
server.listen(common.PORT, function() {
var client = net.connect({port:common.PORT}, function() {
client.on('data', function(chunk) {
assert.fail(null, null, 'this should not be called');
});
client.on('end', common.mustCall(function() {
server.close();
}));
client.write(str);
client.end();
});
});
Loading…
Cancel
Save