Skip to content

Commit

Permalink
http: strip trailing OWS from header values
Browse files Browse the repository at this point in the history
HTTP header values can have trailing OWS, but it should be stripped.  It
is not semantically part of the header's value, and if treated as part
of the value, it can cause spurious inequality between expected and
actual header values.

Note that a single SPC of leading OWS is common before the field-value,
and it is already handled by the HTTP parser by stripping all leading
OWS. It is only the trailing OWS that must be stripped by the parser
user.

	header-field   = field-name ":" OWS field-value OWS
	    ; https://tools.ietf.org/html/rfc7230#section-3.2
	OWS            = *( SP / HTAB )
	    ; https://tools.ietf.org/html/rfc7230#section-3.2.3

Fixes: https://hackerone.com/reports/730779

PR-URL: nodejs-private/node-private#189
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Fedor Indutny <[email protected]>
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: James M Snell <[email protected]>
  • Loading branch information
sam-github authored and BethGriggs committed Feb 1, 2020
1 parent b7da194 commit 25b6897
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 7 deletions.
15 changes: 10 additions & 5 deletions benchmark/http/incoming_headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ const common = require('../common.js');
const http = require('http');

const bench = common.createBenchmark(main, {
// Unicode confuses ab on os x.
c: [50, 500],
n: [0, 5, 20]
c: [50], // Concurrent connections
n: [20], // Number of header lines to append after the common headers
w: [0, 6], // Amount of trailing whitespace
});

function main({ c, n }) {
function main({ c, n, w }) {
const server = http.createServer((req, res) => {
res.end();
});
Expand All @@ -22,7 +22,12 @@ function main({ c, n }) {
'Cache-Control': 'no-cache'
};
for (let i = 0; i < n; i++) {
headers[`foo${i}`] = `some header value ${i}`;
// Note:
// - autocannon does not send header values with OWS
// - wrk can only send trailing OWS. This is a side-effect of wrk
// processing requests with http-parser before sending them, causing
// leading OWS to be stripped.
headers[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`;
}
bench.http({
path: '/',
Expand Down
17 changes: 15 additions & 2 deletions src/node_http_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const uint32_t kOnExecute = 4;
// Any more fields than this will be flushed into JS
const size_t kMaxHeaderFieldsCount = 32;

inline bool IsOWS(char c) {
return c == ' ' || c == '\t';
}

// helper class for the Parser
struct StringPtr {
StringPtr() {
Expand Down Expand Up @@ -136,13 +140,22 @@ struct StringPtr {


Local<String> ToString(Environment* env) const {
if (str_)
if (size_ != 0)
return OneByteString(env->isolate(), str_, size_);
else
return String::Empty(env->isolate());
}


// Strip trailing OWS (SPC or HTAB) from string.
Local<String> ToTrimmedString(Environment* env) {
while (size_ > 0 && IsOWS(str_[size_ - 1])) {
size_--;
}
return ToString(env);
}


const char* str_;
bool on_heap_;
size_t size_;
Expand Down Expand Up @@ -730,7 +743,7 @@ class Parser : public AsyncWrap, public StreamListener {

for (size_t i = 0; i < num_values_; ++i) {
headers_v[i * 2] = fields_[i].ToString(env());
headers_v[i * 2 + 1] = values_[i].ToString(env());
headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env());
}

return Array::New(env()->isolate(), headers_v, num_values_ * 2);
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-http-header-owstext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';
const common = require('../common');

// This test ensures that the http-parser strips leading and trailing OWS from
// header values. It sends the header values in chunks to force the parser to
// build the string up through multiple calls to on_header_value().

const assert = require('assert');
const http = require('http');
const net = require('net');

function check(hdr, snd, rcv) {
const server = http.createServer(common.mustCall((req, res) => {
assert.strictEqual(req.headers[hdr], rcv);
req.pipe(res);
}));

server.listen(0, common.mustCall(function() {
const client = net.connect(this.address().port, start);
function start() {
client.write('GET / HTTP/1.1\r\n' + hdr + ':', drain);
}

function drain() {
if (snd.length === 0) {
return client.write('\r\nConnection: close\r\n\r\n');
}
client.write(snd.shift(), drain);
}

const bufs = [];
client.on('data', function(chunk) {
bufs.push(chunk);
});
client.on('end', common.mustCall(function() {
const head = Buffer.concat(bufs)
.toString('latin1')
.split('\r\n')[0];
assert.strictEqual(head, 'HTTP/1.1 200 OK');
server.close();
}));
}));
}

check('host', [' \t foo.com\t'], 'foo.com');
check('host', [' \t foo\tcom\t'], 'foo\tcom');
check('host', [' \t', ' ', ' foo.com\t', '\t '], 'foo.com');
check('host', [' \t', ' \t'.repeat(100), '\t '], '');
check('host', [' \t', ' - - - - ', '\t '], '- - - -');

0 comments on commit 25b6897

Please sign in to comment.