Skip to content

Commit

Permalink
http: strictly forbid invalid characters from headers
Browse files Browse the repository at this point in the history
PR-URL: nodejs-private/node-private#26
Reviewed-By: Rod Vagg <[email protected]>
Reviewed-By: Сковорода Никита Андреевич <[email protected]>
Reviewed-By: Ben Noordhuis <[email protected]>
  • Loading branch information
jasnell committed Feb 9, 2016
1 parent 4f4c8ab commit 7bef1b7
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 97 deletions.
15 changes: 9 additions & 6 deletions doc/api/http.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -649,8 +649,8 @@ response.addTrailers({'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667'});
response.end();
```

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

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

Expand Down Expand Up @@ -721,8 +721,8 @@ or
response.setHeader('Set-Cookie', ['type=ninja', 'language=javascript']);
```

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

When headers have been set with [`response.setHeader()`][], they will be merged with
any headers passed to [`response.writeHead()`][], with the headers passed to
Expand Down Expand Up @@ -836,8 +836,8 @@ response.writeHead(200, {
This method must only be called once on a message and it must
be called before [`response.end()`][] is called.

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

When headers have been set with [`response.setHeader()`][], they will be merged with
any headers passed to [`response.writeHead()`][], with the headers passed to
Expand All @@ -860,6 +860,9 @@ should be used to determine the number of bytes in a given encoding.
And Node.js does not check whether Content-Length and the length of the body
which has been transmitted are equal or not.

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

## Class: http.IncomingMessage

An `IncomingMessage` object is created by [`http.Server`][] or
Expand Down
17 changes: 17 additions & 0 deletions lib/_http_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,20 @@ function checkIsHttpToken(val) {
return typeof val === 'string' && token.test(val);
}
exports._checkIsHttpToken = checkIsHttpToken;

/**
* True if val contains an invalid field-vchar
* field-value = *( field-content / obs-fold )
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
**/
function checkInvalidHeaderChar(val) {
val = '' + val;
for (var i = 0; i < val.length; i++) {
const ch = val.charCodeAt(i);
if (ch === 9) continue;
if (ch <= 31 || ch > 255 || ch === 127) return true;
}
return false;
}
exports._checkInvalidHeaderChar = checkInvalidHeaderChar;
15 changes: 11 additions & 4 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,10 @@ function storeHeader(self, state, field, value) {
throw new TypeError(
'Header name must be a valid HTTP Token ["' + field + '"]');
}
value = escapeHeaderValue(value);
state.messageHeader += field + ': ' + value + CRLF;
if (common._checkInvalidHeaderChar(value) === true) {
throw new TypeError('The header content contains invalid characters');
}
state.messageHeader += field + ': ' + escapeHeaderValue(value) + CRLF;

if (connectionExpression.test(field)) {
state.sentConnectionHeader = true;
Expand Down Expand Up @@ -341,8 +343,10 @@ OutgoingMessage.prototype.setHeader = function(name, value) {
if (value === undefined)
throw new Error('"value" required in setHeader("' + name + '", value)');
if (this._header)
throw new Error('Can\'t set headers after they are sent');

throw new Error('Can\'t set headers after they are sent.');
if (common._checkInvalidHeaderChar(value) === true) {
throw new TypeError('The header content contains invalid characters');
}
if (this._headers === null)
this._headers = {};

Expand Down Expand Up @@ -515,6 +519,9 @@ OutgoingMessage.prototype.addTrailers = function(headers) {
throw new TypeError(
'Trailer name must be a valid HTTP Token ["' + field + '"]');
}
if (common._checkInvalidHeaderChar(value) === true) {
throw new TypeError('The header content contains invalid characters');
}
this._trailer += field + ': ' + escapeHeaderValue(value) + CRLF;
}
};
Expand Down
87 changes: 0 additions & 87 deletions test/parallel/test-http-header-response-splitting.js

This file was deleted.

55 changes: 55 additions & 0 deletions test/parallel/test-http-response-splitting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const common = require('../common');
const http = require('http');
const net = require('net');
const url = require('url');
const assert = require('assert');

// Response splitting example, credit: Amit Klein, Safebreach
const str = '/welcome?lang=bar%c4%8d%c4%8aContent­Length:%200%c4%8d%c4%8a%c' +
'4%8d%c4%8aHTTP/1.1%20200%20OK%c4%8d%c4%8aContent­Length:%202' +
'0%c4%8d%c4%8aLast­Modified:%20Mon,%2027%20Oct%202003%2014:50:18' +
'%20GMT%c4%8d%c4%8aContent­Type:%20text/html%c4%8d%c4%8a%c4%8' +
'd%c4%8a%3chtml%3eGotcha!%3c/html%3e';

// Response splitting example, credit: Сковорода Никита Андреевич (@ChALkeR)
const x = 'fooഊSet-Cookie: foo=barഊഊ<script>alert("Hi!")</script>';
const y = 'foo⠊Set-Cookie: foo=bar';

var count = 0;

const server = http.createServer((req, res) => {
switch (count++) {
case 0:
const loc = url.parse(req.url, true).query.lang;
assert.throws(common.mustCall(() => {
res.writeHead(302, {Location: `/foo?lang=${loc}`});
}));
break;
case 1:
assert.throws(common.mustCall(() => {
res.writeHead(200, {'foo' : x});
}));
break;
case 2:
assert.throws(common.mustCall(() => {
res.writeHead(200, {'foo' : y});
}));
break;
default:
assert.fail(null, null, 'should not get to here.');
}
if (count === 3)
server.close();
res.end('ok');
});
server.listen(common.PORT, () => {
const end = 'HTTP/1.1\r\n\r\n';
const client = net.connect({port: common.PORT}, () => {
client.write(`GET ${str} ${end}`);
client.write(`GET / ${end}`);
client.write(`GET / ${end}`);
client.end();
});
});

0 comments on commit 7bef1b7

Please sign in to comment.