Permalink
Browse files

http: against HTTP response splitting attack

Fixes #2602.
  • Loading branch information...
koichik committed Jan 26, 2012
1 parent c80abfa commit 2bd9f879779eed958d5a9e518b046ce1184ada52
Showing with 232 additions and 9 deletions.
  1. +14 −4 doc/api/http.markdown
  2. +67 −5 lib/http.js
  3. +151 −0 test/simple/test-http-header-assert.js
View
@@ -286,12 +286,14 @@ Sends a HTTP/1.1 100 Continue message to the client, indicating that
the request body should be sent. See the [checkContinue](#event_checkContinue_) event on
`Server`.
-### response.writeHead(statusCode, [reasonPhrase], [headers])
+### response.writeHead(statusCode, [reasonPhrase], [headers], [noAssert])
Sends a response header to the request. The status code is a 3-digit HTTP
-status code, like `404`. The last argument, `headers`, are the response headers.
+status code, like `404`.
Optionally one can give a human-readable `reasonPhrase` as the second
argument.
+`headers` are the response headers. The header field values are verified
+unless `noAssert` is `true`. An `Error` is thrown if verification fails.
Example:
@@ -326,11 +328,13 @@ Example:
After response header was sent to the client, this property indicates the
status code which was sent out.
-### response.setHeader(name, value)
+### response.setHeader(name, value, [noAssert])
Sets a single header value for implicit headers. If this header already exists
in the to-be-sent headers, its value will be replaced. Use an array of strings
here if you need to send multiple headers with the same name.
+The header field value is verified unless `noAssert` is `true`. An `Error`
+is thrown if verification fails.
Example:
@@ -381,10 +385,12 @@ header information and the first body to the client. The second time
data, and sends that separately. That is, the response is buffered up to the
first chunk of body.
-### response.addTrailers(headers)
+### response.addTrailers(headers, [noAssert])
This method adds HTTP trailing headers (a header but at the end of the
message) to the response.
+The header field values are verified unless `noAssert` is `true`.
+An `Error` is thrown if verification fails.
Trailers will **only** be emitted if chunked encoding is used for the
response; if it is not (e.g., if the request was HTTP/1.0), they will
@@ -428,6 +434,10 @@ Options:
- `path`: Request path. Defaults to `'/'`. Should include query string if any.
E.G. `'/index.html?page=12'`
- `headers`: An object containing request headers.
+ The header field values are verified unless `noAssertHeaders` is `true`.
+ An `Error` is thrown if verification fails.
+- `noAssertHeaders`: If `true`, `headers` field values are not verified.
+ Defaults to `false`.
- `auth`: Basic authentication i.e. `'user:password'` to compute an
Authorization header.
- `agent`: Controls [Agent](#http.Agent) behavior. When an Agent is used
View
@@ -562,7 +562,52 @@ OutgoingMessage.prototype._storeHeader = function(firstLine, headers) {
};
-OutgoingMessage.prototype.setHeader = function(name, value) {
+// RFC 2616 - 2.2 Basic Rules and 4.2 Message Headers
+//
+// field-value = *( field-content | LWS )
+// field-content = <the OCTETs making up the field-value
+// and consisting of either *TEXT or combinations
+// of token, separators, and quoted-string>
+// LWS = [CRLF] 1*( SP | HT )
+// TEXT = <any OCTET except CTLs,
+// but including LWS>
+// OCTET = <any 8-bit sequence of data>
+// CTL = <any US-ASCII control character
+// (octets 0 - 31) and DEL (127)>
+//
+var invalidHeaderValueRegExp =
+ /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\n(?![ \t])|\r(?![ \t\n])/;
+
+function assertHeaderValue(name, value) {
+ var message = 'Invalid header value, ' + name + ': ' + value;
+ if (Array.isArray(value)) {
+ for (var i = 0, len = value.length; i < len; ++i) {
+ assert(!invalidHeaderValueRegExp.test(value[i]), message);
+ }
+ } else {
+ assert(!invalidHeaderValueRegExp.test(value), message);
+ }
+}
+
+function assertHeaderValues(headers) {
+ if (Array.isArray(headers)) {
+ // handle array case
+ // TODO: remove when array is no longer accepted
+ for (var i = 0, len = headers.length; i < len; ++i) {
+ assertHeaderValue(headers[i][0], headers[i][1]);
+ }
+ } else {
+ // handle object case
+ var keys = Object.keys(headers);
+ for (var i = 0, len = keys.length; i < len; ++i) {
+ var key = keys[i];
+ assertHeaderValue(key, headers[key]);
+ }
+ }
+}
+
+
+OutgoingMessage.prototype.setHeader = function(name, value, noAssert) {
if (arguments.length < 2) {
throw new Error("`name` and `value` are required for setHeader().");
}
@@ -571,6 +616,10 @@ OutgoingMessage.prototype.setHeader = function(name, value) {
throw new Error("Can't set headers after they are sent.");
}
+ if (!noAssert) {
+ assertHeaderValue(name, value);
+ }
+
var key = name.toLowerCase();
this._headers = this._headers || {};
this._headerNames = this._headerNames || {};
@@ -665,7 +714,10 @@ OutgoingMessage.prototype.write = function(chunk, encoding) {
};
-OutgoingMessage.prototype.addTrailers = function(headers) {
+OutgoingMessage.prototype.addTrailers = function(headers, noAssert) {
+ if (!noAssert) {
+ assertHeaderValues(headers);
+ }
this._trailer = '';
var keys = Object.keys(headers);
var isArray = (Array.isArray(headers));
@@ -856,6 +908,7 @@ ServerResponse.prototype._implicitHeader = function() {
this.writeHead(this.statusCode);
};
+// writeHead(statusCode, [reasonPhrase], [headers], [noAssertHeaders])
ServerResponse.prototype.writeHead = function(statusCode) {
var reasonPhrase, headers, headerIndex;
@@ -870,6 +923,11 @@ ServerResponse.prototype.writeHead = function(statusCode) {
var obj = arguments[headerIndex];
+ var noAssertHeaders = arguments[headerIndex + 1];
+ if (obj && !noAssertHeaders) {
+ assertHeaderValues(obj);
+ }
+
if (obj && this._headers) {
// Slow-case: when progressive API and header fields are passed.
headers = this._renderHeaders();
@@ -1065,27 +1123,31 @@ function ClientRequest(options, cb) {
self.on('response', cb);
}
+ if (options.headers && !options.noAssertHeaders) {
+ assertHeaderValues(options.headers);
+ }
+
if (!Array.isArray(options.headers)) {
if (options.headers) {
var keys = Object.keys(options.headers);
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
- self.setHeader(key, options.headers[key]);
+ self.setHeader(key, options.headers[key], true);
}
}
if (options.host && !this.getHeader('host') && options.setHost) {
var hostHeader = options.host;
if (options.port && +options.port !== options.defaultPort) {
hostHeader += ':' + options.port;
}
- this.setHeader('Host', hostHeader);
+ this.setHeader('Host', hostHeader, true);
}
}
if (options.auth && !this.getHeader('Authorization')) {
//basic auth
this.setHeader('Authorization', 'Basic ' +
- new Buffer(options.auth).toString('base64'));
+ new Buffer(options.auth).toString('base64'), true);
}
if (method === 'GET' || method === 'HEAD' || method === 'CONNECT') {
@@ -0,0 +1,151 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// 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
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var common = require('../common');
+var assert = require('assert');
+var http = require('http');
+
+var requests = 0;
+
+var server = http.createServer(function(req, res) {
+ ++requests;
+
+ // invalid header values
+ assert.throws(function() {
+ res.writeHead(200, [
+ ['aaa', 'foo'], // valid
+ ['bbb', 'aaa\0bbb'], // CTL
+ ['ccc', 'aaa\rbbb'], // CR but not LWS
+ ['ddd', 'aaa\nbbb'], // LF but not LWS
+ ['eee', 'aaa\r\nbbb'] // CRLF but not LWS
+ ]);
+ });
+ assert.throws(function() {
+ res.setHeader('aaa', 'aaa\0bbb'); // CTL
+ });
+ assert.throws(function() {
+ res.setHeader('aaa', ['aaa', 'bbb\rccc', 'ddd']); // CR but not CTL
+ });
+ assert.throws(function() {
+ res.addTrailers({
+ aaa: 'foo', // valid
+ bbb: 'aaa\0bbb', // CTL
+ ccc: 'aaa\rbbb' // CR but not LWS
+ });
+ });
+
+ // invalid header values but no assert
+ res.setHeader('aaa', 'aaa\0bbb', true); // CTL
+ res.setHeader('aaa', 'aaa\rbbb', true); // CR but not CTL
+
+ // valid header values
+ res.setHeader('aaa', 'foo\r bar\r\tbaz');
+ res.setHeader('bbb', 'foo\n bar\n\tbaz');
+ res.setHeader('ccc', 'foo\r\n bar\r\n\tbaz');
+ res.writeHead(301, {
+ 'Content-Length': 0,
+ 'Location': 'http://example.com',
+ 'Set-Cookie': 'name=vale;\n' +
+ ' expires=Mon, 26-Jan-2012 18:00:00 JST;\r' +
+ '\tpath=/;\r\n' +
+ ' \tdomain=example.com'
+ });
+ res.addTrailers([
+ ['xxx', 'foo\r bar\r\tbaz'],
+ ['yyy', 'foo\n bar\n\tbaz'],
+ ['zzz', 'foo\r\n bar\r\n\tbaz']
+ ]);
+
+ res.end();
+}).listen(common.PORT, function() {
+ // invalid header values
+ assert.throws(function() {
+ http.request({
+ port: common.PORT,
+ headers: {
+ aaa: 'foo', // valid
+ bbb: 'aaa\0bbb', // CTL
+ ccc: 'aaa\rbbb', // CR but not LWS
+ ddd: 'aaa\nbbb', // LF but not LWS
+ eee: 'aaa\r\nbbb' // CRLF but not LWS
+ }
+ });
+ });
+
+ var req = http.request({ port: common.PORT });
+ assert.throws(function() {
+ req.setHeader('aaa', 'aaa\0bbb'); // CTL
+ });
+ assert.throws(function() {
+ req.setHeader('aaa', 'aaa\rbbb'); // CR but not CTL
+ });
+ assert.throws(function() {
+ req.addTrailers([
+ ['aaa', 'foo'], // valid
+ ['bbb', 'aaa\0bbb'], // CTL
+ ['ccc', 'aaa\rbbb'] // CR but not LWS
+ ]);
+ });
+ req.on('error', function() {});
+ req.abort();
+
+ // invalid header values but no assert
+ req = http.request({
+ port: common.PORT,
+ headers: {
+ aaa: 'foo', // valid
+ bbb: 'aaa\0bbb', // CTL
+ ccc: 'aaa\rbbb', // CR but not LWS
+ ddd: 'aaa\nbbb', // LF but not LWS
+ eee: 'aaa\r\nbbb' // CRLF but not LWS
+ },
+ noAssertHeaders: true
+ });
+ req.setHeader('aaa', 'aaa\0bbb', true); // CTL
+ req.setHeader('aaa', 'aaa\rbbb', true); // CR but not CTL
+ req.addTrailers([
+ ['aaa', 'foo'], // valid
+ ['bbb', 'aaa\0bbb'], // CTL
+ ['ccc', 'aaa\rbbb'] // CR but not LWS
+ ], true);
+ req.on('error', function() {});
+ req.abort();
+
+ // valid header values
+ req = http.request({ port: common.PORT });
+ req.setHeader('aaa', 'foo\n bar\r\n\tbaz');
+ req.setHeader('aaa', 'foo\n bar\r\n\tbaz');
+ req.setHeader('aaa', 'foo\n bar\r\n\tbaz');
+ req.addTrailers({
+ xxx: 'foo',
+ yyy: 'bar',
+ zzz: 'baz'
+ });
+
+ req.on('error', function(err) {
+ server.close();
+ });
+ req.end();
+});
+
+process.on('exit', function() {
+ assert.equal(requests, 1);
+});

0 comments on commit 2bd9f87

Please sign in to comment.