Skip to content

Commit 76fd891

Browse files
sam-githubBethGriggs
authored andcommitted
http: strip trailing OWS from header values
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 <matteo.collina@gmail.com> Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 209767c commit 76fd891

File tree

3 files changed

+74
-7
lines changed

3 files changed

+74
-7
lines changed

benchmark/http/incoming_headers.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ const common = require('../common.js');
33
const http = require('http');
44

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

11-
function main({ c, n }) {
11+
function main({ c, n, w }) {
1212
const server = http.createServer((req, res) => {
1313
res.end();
1414
});
@@ -22,7 +22,12 @@ function main({ c, n }) {
2222
'Cache-Control': 'no-cache'
2323
};
2424
for (let i = 0; i < n; i++) {
25-
headers[`foo${i}`] = `some header value ${i}`;
25+
// Note:
26+
// - autocannon does not send header values with OWS
27+
// - wrk can only send trailing OWS. This is a side-effect of wrk
28+
// processing requests with http-parser before sending them, causing
29+
// leading OWS to be stripped.
30+
headers[`foo${i}`] = `some header value ${i}${' \t'.repeat(w / 2)}`;
2631
}
2732
bench.http({
2833
path: '/',

src/node_http_parser_impl.h

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ const uint32_t kOnExecute = 4;
8181
// Any more fields than this will be flushed into JS
8282
const size_t kMaxHeaderFieldsCount = 32;
8383

84+
inline bool IsOWS(char c) {
85+
return c == ' ' || c == '\t';
86+
}
87+
8488
// helper class for the Parser
8589
struct StringPtr {
8690
StringPtr() {
@@ -140,13 +144,22 @@ struct StringPtr {
140144

141145

142146
Local<String> ToString(Environment* env) const {
143-
if (str_)
147+
if (size_ != 0)
144148
return OneByteString(env->isolate(), str_, size_);
145149
else
146150
return String::Empty(env->isolate());
147151
}
148152

149153

154+
// Strip trailing OWS (SPC or HTAB) from string.
155+
Local<String> ToTrimmedString(Environment* env) {
156+
while (size_ > 0 && IsOWS(str_[size_ - 1])) {
157+
size_--;
158+
}
159+
return ToString(env);
160+
}
161+
162+
150163
const char* str_;
151164
bool on_heap_;
152165
size_t size_;
@@ -765,7 +778,7 @@ class Parser : public AsyncWrap, public StreamListener {
765778

766779
for (size_t i = 0; i < num_values_; ++i) {
767780
headers_v[i * 2] = fields_[i].ToString(env());
768-
headers_v[i * 2 + 1] = values_[i].ToString(env());
781+
headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env());
769782
}
770783

771784
return Array::New(env()->isolate(), headers_v, num_values_ * 2);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
// This test ensures that the http-parser strips leading and trailing OWS from
5+
// header values. It sends the header values in chunks to force the parser to
6+
// build the string up through multiple calls to on_header_value().
7+
8+
const assert = require('assert');
9+
const http = require('http');
10+
const net = require('net');
11+
12+
function check(hdr, snd, rcv) {
13+
const server = http.createServer(common.mustCall((req, res) => {
14+
assert.strictEqual(req.headers[hdr], rcv);
15+
req.pipe(res);
16+
}));
17+
18+
server.listen(0, common.mustCall(function() {
19+
const client = net.connect(this.address().port, start);
20+
function start() {
21+
client.write('GET / HTTP/1.1\r\n' + hdr + ':', drain);
22+
}
23+
24+
function drain() {
25+
if (snd.length === 0) {
26+
return client.write('\r\nConnection: close\r\n\r\n');
27+
}
28+
client.write(snd.shift(), drain);
29+
}
30+
31+
const bufs = [];
32+
client.on('data', function(chunk) {
33+
bufs.push(chunk);
34+
});
35+
client.on('end', common.mustCall(function() {
36+
const head = Buffer.concat(bufs)
37+
.toString('latin1')
38+
.split('\r\n')[0];
39+
assert.strictEqual(head, 'HTTP/1.1 200 OK');
40+
server.close();
41+
}));
42+
}));
43+
}
44+
45+
check('host', [' \t foo.com\t'], 'foo.com');
46+
check('host', [' \t foo\tcom\t'], 'foo\tcom');
47+
check('host', [' \t', ' ', ' foo.com\t', '\t '], 'foo.com');
48+
check('host', [' \t', ' \t'.repeat(100), '\t '], '');
49+
check('host', [' \t', ' - - - - ', '\t '], '- - - -');

0 commit comments

Comments
 (0)