Skip to content

Commit cf2b714

Browse files
committed
http: strictly forbid invalid characters from headers
PR-URL: nodejs-private/node-private#20
1 parent 0eae95e commit cf2b714

File tree

4 files changed

+121
-10
lines changed

4 files changed

+121
-10
lines changed

doc/api/http.markdown

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -614,8 +614,8 @@ emit trailers, with a list of the header fields in its value. E.g.,
614614
response.addTrailers({'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667'});
615615
response.end();
616616

617-
Attempting to set a trailer field name that contains invalid characters will
618-
result in a [`TypeError`][] being thrown.
617+
Attempting to set a header field name or value that contains invalid characters
618+
will result in a [`TypeError`][] being thrown.
619619

620620
### response.end([data][, encoding][, callback])
621621

@@ -678,8 +678,8 @@ or
678678

679679
response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']);
680680

681-
Attempting to set a header field name that contains invalid characters will
682-
result in a [`TypeError`][] being thrown.
681+
Attempting to set a header field name or value that contains invalid characters
682+
will result in a [`TypeError`][] being thrown.
683683

684684
### response.setTimeout(msecs, callback)
685685

@@ -773,8 +773,8 @@ Example:
773773
This method must only be called once on a message and it must
774774
be called before [`response.end()`][] is called.
775775

776-
If you call [`response.write()`][] or [`response.end()`][] before calling this, the
777-
implicit/mutable headers will be calculated and call this function for you.
776+
If you call [`response.write()`][] or [`response.end()`][] before calling this,
777+
the implicit/mutable headers will be calculated and call this function for you.
778778

779779
Note that Content-Length is given in bytes not characters. The above example
780780
works because the string `'hello world'` contains only single byte characters.
@@ -783,6 +783,9 @@ should be used to determine the number of bytes in a given encoding.
783783
And Node.js does not check whether Content-Length and the length of the body
784784
which has been transmitted are equal or not.
785785

786+
Attempting to set a header field name or value that contains invalid characters
787+
will result in a [`TypeError`][] being thrown.
788+
786789
## Class: http.IncomingMessage
787790

788791
An `IncomingMessage` object is created by [`http.Server`][] or

lib/_http_common.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,30 @@ function httpSocketSetup(socket) {
199199
socket.on('drain', ondrain);
200200
}
201201
exports.httpSocketSetup = httpSocketSetup;
202+
203+
/**
204+
* Verifies that the given val is a valid HTTP token
205+
* per the rules defined in RFC 7230
206+
**/
207+
const token = /^[a-zA-Z0-9_!#$%&'*+.^`|~-]+$/;
208+
function checkIsHttpToken(val) {
209+
return typeof val === 'string' && token.test(val);
210+
}
211+
exports._checkIsHttpToken = checkIsHttpToken;
212+
213+
/**
214+
* True if val contains an invalid field-vchar
215+
* field-value = *( field-content / obs-fold )
216+
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
217+
* field-vchar = VCHAR / obs-text
218+
**/
219+
function checkInvalidHeaderChar(val) {
220+
val = '' + val;
221+
for (var i = 0; i < val.length; i++) {
222+
const ch = val.charCodeAt(i);
223+
if (ch === 9) continue;
224+
if (ch <= 31 || ch > 255 || ch === 127) return true;
225+
}
226+
return false;
227+
}
228+
exports._checkInvalidHeaderChar = checkInvalidHeaderChar;

lib/_http_outgoing.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const contentLengthExpression = /^Content-Length$/i;
1919
const dateExpression = /^Date$/i;
2020
const expectExpression = /^Expect$/i;
2121
const trailerExpression = /^Trailer$/i;
22+
const lenientHttpHeaders = !!process.REVERT_CVE_2016_2216;
2223

2324
const automaticHeaders = {
2425
connection: true,
@@ -299,8 +300,16 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
299300
};
300301

301302
function storeHeader(self, state, field, value) {
302-
value = escapeHeaderValue(value);
303-
state.messageHeader += field + ': ' + value + CRLF;
303+
if (!lenientHttpHeaders) {
304+
if (!common._checkIsHttpToken(field)) {
305+
throw new TypeError(
306+
'Header name must be a valid HTTP Token ["' + field + '"]');
307+
}
308+
if (common._checkInvalidHeaderChar(value) === true) {
309+
throw new TypeError('The header content contains invalid characters');
310+
}
311+
}
312+
state.messageHeader += field + ': ' + escapeHeaderValue(value) + CRLF;
304313

305314
if (connectionExpression.test(field)) {
306315
state.sentConnectionHeader = true;
@@ -333,7 +342,15 @@ OutgoingMessage.prototype.setHeader = function(name, value) {
333342
throw new Error('`value` required in setHeader("' + name + '", value).');
334343
if (this._header)
335344
throw new Error('Can\'t set headers after they are sent.');
336-
345+
if (!lenientHttpHeaders) {
346+
if (!common._checkIsHttpToken(name)) {
347+
throw new TypeError(
348+
'Trailer name must be a valid HTTP Token ["' + name + '"]');
349+
}
350+
if (common._checkInvalidHeaderChar(value) === true) {
351+
throw new TypeError('The header content contains invalid characters');
352+
}
353+
}
337354
if (this._headers === null)
338355
this._headers = {};
339356

@@ -482,6 +499,7 @@ function connectionCorkNT(conn) {
482499

483500

484501
function escapeHeaderValue(value) {
502+
if (!lenientHttpHeaders) return value;
485503
// Protect against response splitting. The regex test is there to
486504
// minimize the performance impact in the common case.
487505
return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value;
@@ -502,7 +520,15 @@ OutgoingMessage.prototype.addTrailers = function(headers) {
502520
field = key;
503521
value = headers[key];
504522
}
505-
523+
if (!lenientHttpHeaders) {
524+
if (!common._checkIsHttpToken(field)) {
525+
throw new TypeError(
526+
'Trailer name must be a valid HTTP Token ["' + field + '"]');
527+
}
528+
if (common._checkInvalidHeaderChar(value) === true) {
529+
throw new TypeError('The header content contains invalid characters');
530+
}
531+
}
506532
this._trailer += field + ': ' + escapeHeaderValue(value) + CRLF;
507533
}
508534
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const http = require('http');
5+
const net = require('net');
6+
const url = require('url');
7+
const assert = require('assert');
8+
9+
// Response splitting example, credit: Amit Klein, Safebreach
10+
const str = '/welcome?lang=bar%c4%8d%c4%8aContent­Length:%200%c4%8d%c4%8a%c' +
11+
'4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContent­Length:%202' +
12+
'0%c4%8d%c4%8aLast­Modified:%20Mon,%2027%20Oct%202003%2014:50:18' +
13+
'%20GMT%c4%8d%c4%8aContent­Type:%20text/html%c4%8d%c4%8a%c4%8' +
14+
'd%c4%8a%3chtml%3eGotcha!%3c/html%3e';
15+
16+
// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR)
17+
const x = 'fooഊSet-Cookie: foo=barഊഊ<script>alert("Hi!")</script>';
18+
const y = 'foo⠊Set-Cookie: foo=bar';
19+
20+
var count = 0;
21+
22+
const server = http.createServer((req, res) => {
23+
switch (count++) {
24+
case 0:
25+
const loc = url.parse(req.url, true).query.lang;
26+
assert.throws(common.mustCall(() => {
27+
res.writeHead(302, {Location: `/foo?lang=${loc}`});
28+
}));
29+
break;
30+
case 1:
31+
assert.throws(common.mustCall(() => {
32+
res.writeHead(200, {'foo' : x});
33+
}));
34+
break;
35+
case 2:
36+
assert.throws(common.mustCall(() => {
37+
res.writeHead(200, {'foo' : y});
38+
}));
39+
break;
40+
default:
41+
assert.fail(null, null, 'should not get to here.');
42+
}
43+
if (count === 3)
44+
server.close();
45+
res.end('ok');
46+
});
47+
server.listen(common.PORT, () => {
48+
const end = 'HTTP/1.1\r\n\r\n';
49+
const client = net.connect({port: common.PORT}, () => {
50+
client.write(`GET ${str} ${end}`);
51+
client.write(`GET / ${end}`);
52+
client.write(`GET / ${end}`);
53+
client.end();
54+
});
55+
});

0 commit comments

Comments
 (0)