Skip to content

Commit df08d52

Browse files
mcollinaShogunPandaronag
authored andcommitted
http: add requestTimeout
This commits introduces a new http.Server option called requestTimeout with a default value in milliseconds of 0. If requestTimeout is set to a positive value, the server will start a new timer set to expire in requestTimeout milliseconds when a new connection is established. The timer is also set again if new requests after the first are received on the socket (this handles pipelining and keep-alive cases). The timer is cancelled when: 1. the request body is completely received by the server. 2. the response is completed. This handles the case where the application responds to the client without consuming the request body. 3. the connection is upgraded, like in the WebSocket case. If the timer expires, then the server responds with status code 408 and closes the connection. CVE-2020-8251 PR-URL: nodejs-private/node-private#208 Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Mary Marchini <[email protected]> Co-Authored-By: Paolo Insogna <[email protected]> Co-Authored-By: Robert Nagy <[email protected]>
1 parent cb90248 commit df08d52

14 files changed

+517
-8
lines changed

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,11 @@ allowed size for a `Buffer`.
940940
An invalid symlink type was passed to the [`fs.symlink()`][] or
941941
[`fs.symlinkSync()`][] methods.
942942

943+
<a id="ERR_HTTP_REQUEST_TIMEOUT"></a>
944+
### `ERR_HTTP_REQUEST_TIMEOUT`
945+
946+
The client has not sent the entire request within the allowed time.
947+
943948
<a id="ERR_HTTP_HEADERS_SENT"></a>
944949
### `ERR_HTTP_HEADERS_SENT`
945950

doc/api/http.md

+17
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,23 @@ added: v0.7.0
12591259

12601260
Limits maximum incoming headers count. If set to 0, no limit will be applied.
12611261

1262+
### `server.requestTimeout`
1263+
<!-- YAML
1264+
added: REPLACEME
1265+
-->
1266+
1267+
* {number} **Default:** `0`
1268+
1269+
Sets the timeout value in milliseconds for receiving the entire request from
1270+
the client.
1271+
1272+
If the timeout expires, the server responds with status 408 without
1273+
forwarding the request to the request listener and then closes the connection.
1274+
1275+
It must be set to a non-zero value (e.g. 120 seconds) to proctect against
1276+
potential Denial-of-Service attacks in case the server is deployed without a
1277+
reverse proxy in front.
1278+
12621279
### `server.setTimeout([msecs][, callback])`
12631280
<!-- YAML
12641281
added: v0.9.12

doc/api/https.md

+10
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ This method is identical to [`server.listen()`][] from [`net.Server`][].
113113

114114
See [`http.Server#maxHeadersCount`][].
115115

116+
### `server.requestTimeout`
117+
<!-- YAML
118+
added: REPLACEME
119+
-->
120+
121+
* {number} **Default:** `0`
122+
123+
See [`http.Server#requestTimeout`][].
124+
116125
### `server.setTimeout([msecs][, callback])`
117126
<!-- YAML
118127
added: v0.11.2
@@ -451,6 +460,7 @@ headers: max-age=0; pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18="; p
451460
[`http.Server#headersTimeout`]: http.html#http_server_headerstimeout
452461
[`http.Server#keepAliveTimeout`]: http.html#http_server_keepalivetimeout
453462
[`http.Server#maxHeadersCount`]: http.html#http_server_maxheaderscount
463+
[`http.Server#requestTimeout`]: http.html#http_server_requesttimeout
454464
[`http.Server#setTimeout()`]: http.html#http_server_settimeout_msecs_callback
455465
[`http.Server#timeout`]: http.html#http_server_timeout
456466
[`http.Server`]: http.html#http_class_http_server

lib/_http_common.js

+8
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
4444
});
4545

4646
const kIncomingMessage = Symbol('IncomingMessage');
47+
const kRequestTimeout = Symbol('RequestTimeout');
4748
const kOnHeaders = HTTPParser.kOnHeaders | 0;
4849
const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;
4950
const kOnBody = HTTPParser.kOnBody | 0;
@@ -99,6 +100,12 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
99100
incoming.url = url;
100101
incoming.upgrade = upgrade;
101102

103+
if (socket) {
104+
debug('requestTimeout timer moved to req');
105+
incoming[kRequestTimeout] = incoming.socket[kRequestTimeout];
106+
incoming.socket[kRequestTimeout] = undefined;
107+
}
108+
102109
let n = headers.length;
103110

104111
// If parser.maxHeaderPairs <= 0 assume that there's no limit.
@@ -264,6 +271,7 @@ module.exports = {
264271
methods,
265272
parsers,
266273
kIncomingMessage,
274+
kRequestTimeout,
267275
HTTPParser,
268276
isLenient,
269277
prepareError,

lib/_http_server.js

+83-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const {
4040
continueExpression,
4141
chunkExpression,
4242
kIncomingMessage,
43+
kRequestTimeout,
4344
HTTPParser,
4445
isLenient,
4546
_checkInvalidHeaderChar: checkInvalidHeaderChar,
@@ -61,6 +62,7 @@ const {
6162
codes
6263
} = require('internal/errors');
6364
const {
65+
ERR_HTTP_REQUEST_TIMEOUT,
6466
ERR_HTTP_HEADERS_SENT,
6567
ERR_HTTP_INVALID_STATUS_CODE,
6668
ERR_HTTP_SOCKET_ENCODING,
@@ -77,6 +79,7 @@ const {
7779
DTRACE_HTTP_SERVER_RESPONSE
7880
} = require('internal/dtrace');
7981
const { observerCounts, constants } = internalBinding('performance');
82+
const { setTimeout, clearTimeout } = require('timers');
8083
const { NODE_PERFORMANCE_ENTRY_TYPE_HTTP } = constants;
8184

8285
const kServerResponse = Symbol('ServerResponse');
@@ -148,6 +151,7 @@ const STATUS_CODES = {
148151
511: 'Network Authentication Required' // RFC 6585 6
149152
};
150153

154+
const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0;
151155
const kOnExecute = HTTPParser.kOnExecute | 0;
152156
const kOnTimeout = HTTPParser.kOnTimeout | 0;
153157

@@ -369,6 +373,7 @@ function Server(options, requestListener) {
369373
this.keepAliveTimeout = 5000;
370374
this.maxHeadersCount = null;
371375
this.headersTimeout = 60 * 1000; // 60 seconds
376+
this.requestTimeout = 0; // 120 seconds
372377
}
373378
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
374379
ObjectSetPrototypeOf(Server, net.Server);
@@ -491,6 +496,16 @@ function connectionListenerInternal(server, socket) {
491496
parser[kOnTimeout] =
492497
onParserTimeout.bind(undefined, server, socket);
493498

499+
// When receiving new requests on the same socket (pipelining or keep alive)
500+
// make sure the requestTimeout is active.
501+
parser[kOnMessageBegin] =
502+
setRequestTimeout.bind(undefined, server, socket);
503+
504+
// This protects from DOS attack where an attacker establish the connection
505+
// without sending any data on applications where server.timeout is left to
506+
// the default value of zero.
507+
setRequestTimeout(server, socket);
508+
494509
socket._paused = false;
495510
}
496511

@@ -586,6 +601,11 @@ function socketOnData(server, socket, parser, state, d) {
586601
onParserExecuteCommon(server, socket, parser, state, ret, d);
587602
}
588603

604+
function onRequestTimeout(socket) {
605+
socket[kRequestTimeout] = undefined;
606+
socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT());
607+
}
608+
589609
function onParserExecute(server, socket, parser, state, ret) {
590610
// When underlying `net.Socket` instance is consumed - no
591611
// `data` events are emitted, and thus `socket.setTimeout` fires the
@@ -608,6 +628,10 @@ const badRequestResponse = Buffer.from(
608628
`HTTP/1.1 400 ${STATUS_CODES[400]}${CRLF}` +
609629
`Connection: close${CRLF}${CRLF}`, 'ascii'
610630
);
631+
const requestTimeoutResponse = Buffer.from(
632+
`HTTP/1.1 408 ${STATUS_CODES[408]}${CRLF}` +
633+
`Connection: close${CRLF}${CRLF}`, 'ascii'
634+
);
611635
const requestHeaderFieldsTooLargeResponse = Buffer.from(
612636
`HTTP/1.1 431 ${STATUS_CODES[431]}${CRLF}` +
613637
`Connection: close${CRLF}${CRLF}`, 'ascii'
@@ -619,8 +643,20 @@ function socketOnError(e) {
619643

620644
if (!this.server.emit('clientError', e, this)) {
621645
if (this.writable && this.bytesWritten === 0) {
622-
const response = e.code === 'HPE_HEADER_OVERFLOW' ?
623-
requestHeaderFieldsTooLargeResponse : badRequestResponse;
646+
let response;
647+
648+
switch (e.code) {
649+
case 'HPE_HEADER_OVERFLOW':
650+
response = requestHeaderFieldsTooLargeResponse;
651+
break;
652+
case 'ERR_HTTP_REQUEST_TIMEOUT':
653+
response = requestTimeoutResponse;
654+
break;
655+
default:
656+
response = badRequestResponse;
657+
break;
658+
}
659+
624660
this.write(response);
625661
}
626662
this.destroy(e);
@@ -660,11 +696,20 @@ function onParserExecuteCommon(server, socket, parser, state, ret, d) {
660696
const bodyHead = d.slice(ret, d.length);
661697

662698
socket.readableFlowing = null;
699+
700+
// Clear the requestTimeout after upgrading the connection.
701+
clearRequestTimeout(req);
702+
663703
server.emit(eventName, req, socket, bodyHead);
664704
} else {
665705
// Got CONNECT method, but have no handler.
666706
socket.destroy();
667707
}
708+
} else {
709+
// When receiving new requests on the same socket (pipelining or keep alive)
710+
// make sure the requestTimeout is active.
711+
parser[kOnMessageBegin] =
712+
setRequestTimeout.bind(undefined, server, socket);
668713
}
669714

670715
if (socket._paused && socket.parser) {
@@ -692,6 +737,32 @@ function clearIncoming(req) {
692737
}
693738
}
694739

740+
function setRequestTimeout(server, socket) {
741+
// Set the request timeout handler.
742+
if (
743+
!socket[kRequestTimeout] &&
744+
server.requestTimeout && server.requestTimeout > 0
745+
) {
746+
debug('requestTimeout timer set');
747+
socket[kRequestTimeout] =
748+
setTimeout(onRequestTimeout, server.requestTimeout, socket).unref();
749+
}
750+
}
751+
752+
function clearRequestTimeout(req) {
753+
if (!req) {
754+
req = this;
755+
}
756+
757+
if (!req[kRequestTimeout]) {
758+
return;
759+
}
760+
761+
debug('requestTimeout timer cleared');
762+
clearTimeout(req[kRequestTimeout]);
763+
req[kRequestTimeout] = undefined;
764+
}
765+
695766
function resOnFinish(req, res, socket, state, server) {
696767
// Usually the first incoming element should be our request. it may
697768
// be that in the case abortIncoming() was called that the incoming
@@ -706,6 +777,14 @@ function resOnFinish(req, res, socket, state, server) {
706777
if (!req._consuming && !req._readableState.resumeScheduled)
707778
req._dump();
708779

780+
// Make sure the requestTimeout is cleared before finishing.
781+
// This might occur if the application has sent a response
782+
// without consuming the request body, which would have alredy
783+
// cleared the timer.
784+
// clearRequestTimeout can be executed even if the timer is not active,
785+
// so this is safe.
786+
clearRequestTimeout(req);
787+
709788
res.detachSocket(socket);
710789
clearIncoming(req);
711790
process.nextTick(emitCloseNT, res);
@@ -802,6 +881,8 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
802881
res.end();
803882
}
804883
} else {
884+
req.on('end', clearRequestTimeout);
885+
805886
server.emit('request', req, res);
806887
}
807888
return 0; // No special treatment.

lib/https.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function Server(opts, requestListener) {
8080
this.keepAliveTimeout = 5000;
8181
this.maxHeadersCount = null;
8282
this.headersTimeout = 60 * 1000; // 60 seconds
83+
this.requestTimeout = 120 * 1000; // 120 seconds
8384
}
8485
ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype);
8586
ObjectSetPrototypeOf(Server, tls.Server);

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,7 @@ E('ERR_HTTP_HEADERS_SENT',
940940
E('ERR_HTTP_INVALID_HEADER_VALUE',
941941
'Invalid value "%s" for header "%s"', TypeError);
942942
E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError);
943+
E('ERR_HTTP_REQUEST_TIMEOUT', 'Request timeout', Error);
943944
E('ERR_HTTP_SOCKET_ENCODING',
944945
'Changing the socket encoding is not allowed per RFC7230 Section 3.', Error);
945946
E('ERR_HTTP_TRAILER_INVALID',

src/node_http_parser.cc

+22-6
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,13 @@ using v8::Uint32;
6969
using v8::Undefined;
7070
using v8::Value;
7171

72-
const uint32_t kOnHeaders = 0;
73-
const uint32_t kOnHeadersComplete = 1;
74-
const uint32_t kOnBody = 2;
75-
const uint32_t kOnMessageComplete = 3;
76-
const uint32_t kOnExecute = 4;
77-
const uint32_t kOnTimeout = 5;
72+
const uint32_t kOnMessageBegin = 0;
73+
const uint32_t kOnHeaders = 1;
74+
const uint32_t kOnHeadersComplete = 2;
75+
const uint32_t kOnBody = 3;
76+
const uint32_t kOnMessageComplete = 4;
77+
const uint32_t kOnExecute = 5;
78+
const uint32_t kOnTimeout = 6;
7879
// Any more fields than this will be flushed into JS
7980
const size_t kMaxHeaderFieldsCount = 32;
8081

@@ -204,6 +205,19 @@ class Parser : public AsyncWrap, public StreamListener {
204205
url_.Reset();
205206
status_message_.Reset();
206207
header_parsing_start_time_ = uv_hrtime();
208+
209+
Local<Value> cb = object()->Get(env()->context(), kOnMessageBegin)
210+
.ToLocalChecked();
211+
if (cb->IsFunction()) {
212+
InternalCallbackScope callback_scope(
213+
this, InternalCallbackScope::kSkipTaskQueues);
214+
215+
MaybeLocal<Value> r = cb.As<Function>()->Call(
216+
env()->context(), object(), 0, nullptr);
217+
218+
if (r.IsEmpty()) callback_scope.MarkAsFailed();
219+
}
220+
207221
return 0;
208222
}
209223

@@ -939,6 +953,8 @@ void InitializeHttpParser(Local<Object> target,
939953
Integer::New(env->isolate(), HTTP_REQUEST));
940954
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "RESPONSE"),
941955
Integer::New(env->isolate(), HTTP_RESPONSE));
956+
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnMessageBegin"),
957+
Integer::NewFromUnsigned(env->isolate(), kOnMessageBegin));
942958
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnHeaders"),
943959
Integer::NewFromUnsigned(env->isolate(), kOnHeaders));
944960
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kOnHeadersComplete"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { createServer } = require('http');
6+
const { connect } = require('net');
7+
8+
// This test validates that the server returns 408
9+
// after server.requestTimeout if the client
10+
// pauses before start sending the body.
11+
12+
const server = createServer(common.mustCall((req, res) => {
13+
let body = '';
14+
req.setEncoding('utf-8');
15+
16+
req.on('data', (chunk) => {
17+
body += chunk;
18+
});
19+
20+
req.on('end', () => {
21+
res.writeHead(200, { 'Content-Type': 'text/plain' });
22+
res.write(body);
23+
res.end();
24+
});
25+
}));
26+
27+
// 0 seconds is the default
28+
assert.strictEqual(server.requestTimeout, 0);
29+
const requestTimeout = common.platformTimeout(1000);
30+
server.requestTimeout = requestTimeout;
31+
assert.strictEqual(server.requestTimeout, requestTimeout);
32+
33+
server.listen(0, common.mustCall(() => {
34+
const client = connect(server.address().port);
35+
let response = '';
36+
37+
client.on('data', common.mustCall((chunk) => {
38+
response += chunk.toString('utf-8');
39+
}));
40+
41+
client.resume();
42+
client.write('POST / HTTP/1.1\r\n');
43+
client.write('Content-Length: 20\r\n');
44+
client.write('Connection: close\r\n');
45+
client.write('\r\n');
46+
47+
setTimeout(() => {
48+
client.write('12345678901234567890\r\n\r\n');
49+
}, common.platformTimeout(2000)).unref();
50+
51+
const errOrEnd = common.mustCall(function(err) {
52+
console.log(err);
53+
assert.strictEqual(
54+
response,
55+
'HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n'
56+
);
57+
server.close();
58+
});
59+
60+
client.on('end', errOrEnd);
61+
client.on('error', errOrEnd);
62+
}));

0 commit comments

Comments
 (0)