Skip to content

Commit f158656

Browse files
sidwebworksRafaelGSS
authored andcommitted
http: throw error on content-length mismatch
PR-URL: #44378 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Robert Nagy <ronagy@icloud.com>
1 parent c5c1bc4 commit f158656

7 files changed

+140
-8
lines changed

doc/api/errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,12 @@ When using [`fs.cp()`][], `src` or `dest` pointed to an invalid path.
13251325

13261326
<a id="ERR_FS_CP_FIFO_PIPE"></a>
13271327

1328+
### `ERR_HTTP_CONTENT_LENGTH_MISMATCH`
1329+
1330+
Response body size doesn't match with the specified content-length header value.
1331+
1332+
<a id="ERR_HTTP_CONTENT_LENGTH_MISMATCH"></a>
1333+
13281334
### `ERR_FS_CP_FIFO_PIPE`
13291335

13301336
<!--

doc/api/http.md

+10-5
Original file line numberDiff line numberDiff line change
@@ -421,8 +421,12 @@ the data is read it will consume memory that can eventually lead to a
421421
For backward compatibility, `res` will only emit `'error'` if there is an
422422
`'error'` listener registered.
423423

424-
Node.js does not check whether Content-Length and the length of the
425-
body which has been transmitted are equal or not.
424+
Set `Content-Length` header to limit the response body size. Mismatching the
425+
`Content-Length` header value will result in an \[`Error`]\[] being thrown,
426+
identified by `code:` [`'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`][].
427+
428+
`Content-Length` value should be in bytes, not characters. Use
429+
[`Buffer.byteLength()`][] to determine the length of the body in bytes.
426430

427431
### Event: `'abort'`
428432

@@ -2196,13 +2200,13 @@ const server = http.createServer((req, res) => {
21962200
});
21972201
```
21982202

2199-
`Content-Length` is given in bytes, not characters. Use
2203+
`Content-Length` is read in bytes, not characters. Use
22002204
[`Buffer.byteLength()`][] to determine the length of the body in bytes. Node.js
2201-
does not check whether `Content-Length` and the length of the body which has
2205+
will check whether `Content-Length` and the length of the body which has
22022206
been transmitted are equal or not.
22032207

22042208
Attempting to set a header field name or value that contains invalid characters
2205-
will result in a [`TypeError`][] being thrown.
2209+
will result in a \[`Error`]\[] being thrown.
22062210

22072211
### `response.writeProcessing()`
22082212

@@ -3626,6 +3630,7 @@ added: v18.8.0
36263630
Set the maximum number of idle HTTP parsers. **Default:** `1000`.
36273631

36283632
[RFC 8187]: https://www.rfc-editor.org/rfc/rfc8187.txt
3633+
[`'ERR_HTTP_CONTENT_LENGTH_MISMATCH'`]: errors.md#err_http_content_length_mismatch
36293634
[`'checkContinue'`]: #event-checkcontinue
36303635
[`'finish'`]: #event-finish
36313636
[`'request'`]: #event-request

lib/_http_outgoing.js

+40-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const {
2525
Array,
2626
ArrayIsArray,
2727
ArrayPrototypeJoin,
28+
MathAbs,
2829
MathFloor,
2930
NumberPrototypeToString,
3031
ObjectCreate,
@@ -57,6 +58,7 @@ const {
5758
} = require('internal/async_hooks');
5859
const {
5960
codes: {
61+
ERR_HTTP_CONTENT_LENGTH_MISMATCH,
6062
ERR_HTTP_HEADERS_SENT,
6163
ERR_HTTP_INVALID_HEADER_VALUE,
6264
ERR_HTTP_TRAILER_INVALID,
@@ -84,6 +86,8 @@ const HIGH_WATER_MARK = getDefaultHighWaterMark();
8486

8587
const kCorked = Symbol('corked');
8688
const kUniqueHeaders = Symbol('kUniqueHeaders');
89+
const kBytesWritten = Symbol('kBytesWritten');
90+
const kEndCalled = Symbol('kEndCalled');
8791

8892
const nop = () => {};
8993

@@ -123,6 +127,9 @@ function OutgoingMessage() {
123127
this._removedContLen = false;
124128
this._removedTE = false;
125129

130+
this.strictContentLength = false;
131+
this[kBytesWritten] = 0;
132+
this[kEndCalled] = false;
126133
this._contentLength = null;
127134
this._hasBody = true;
128135
this._trailer = '';
@@ -330,7 +337,9 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
330337
// This is a shameful hack to get the headers and first body chunk onto
331338
// the same packet. Future versions of Node are going to take care of
332339
// this at a lower level and in a more general way.
333-
if (!this._headerSent) {
340+
if (!this._headerSent && this._header !== null) {
341+
// `this._header` can be null if OutgoingMessage is used without a proper Socket
342+
// See: /test/parallel/test-http-outgoing-message-inheritance.js
334343
if (typeof data === 'string' &&
335344
(encoding === 'utf8' || encoding === 'latin1' || !encoding)) {
336345
data = this._header + data;
@@ -349,6 +358,14 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
349358
return this._writeRaw(data, encoding, callback);
350359
};
351360

361+
function _getMessageBodySize(chunk, headers, encoding) {
362+
if (Buffer.isBuffer(chunk)) return chunk.length;
363+
const chunkLength = chunk ? Buffer.byteLength(chunk, encoding) : 0;
364+
const headerLength = headers ? headers.length : 0;
365+
if (headerLength === chunkLength) return 0;
366+
if (headerLength < chunkLength) return MathAbs(chunkLength - headerLength);
367+
return chunkLength;
368+
}
352369

353370
OutgoingMessage.prototype._writeRaw = _writeRaw;
354371
function _writeRaw(data, encoding, callback) {
@@ -364,6 +381,25 @@ function _writeRaw(data, encoding, callback) {
364381
encoding = null;
365382
}
366383

384+
// TODO(sidwebworks): flip the `strictContentLength` default to `true` in a future PR
385+
if (this.strictContentLength && conn && conn.writable && !this._removedContLen && this._hasBody) {
386+
const skip = conn._httpMessage.statusCode === 304 || (this.hasHeader('transfer-encoding') || this.chunkedEncoding);
387+
388+
if (typeof this._contentLength === 'number' && !skip) {
389+
const size = _getMessageBodySize(data, conn._httpMessage._header, encoding);
390+
391+
if ((size + this[kBytesWritten]) > this._contentLength) {
392+
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
393+
}
394+
395+
if (this[kEndCalled] && (size + this[kBytesWritten]) !== this._contentLength) {
396+
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
397+
}
398+
399+
this[kBytesWritten] += size;
400+
}
401+
}
402+
367403
if (conn && conn._httpMessage === this && conn.writable) {
368404
// There might be pending data in the this.output buffer.
369405
if (this.outputData.length) {
@@ -559,6 +595,7 @@ function matchHeader(self, state, field, value) {
559595
break;
560596
case 'content-length':
561597
state.contLen = true;
598+
self._contentLength = value;
562599
self._removedContLen = false;
563600
break;
564601
case 'date':
@@ -923,6 +960,8 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
923960
encoding = null;
924961
}
925962

963+
this[kEndCalled] = true;
964+
926965
if (chunk) {
927966
if (this.finished) {
928967
onError(this,

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,8 @@ E('ERR_HTTP2_TRAILERS_NOT_READY',
11421142
'Trailing headers cannot be sent until after the wantTrailers event is ' +
11431143
'emitted', Error);
11441144
E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error);
1145+
E('ERR_HTTP_CONTENT_LENGTH_MISMATCH',
1146+
'Response body\'s content-length of %s byte(s) does not match the content-length of %s byte(s) set in header', Error);
11451147
E('ERR_HTTP_HEADERS_SENT',
11461148
'Cannot %s headers after they are sent to the client', Error);
11471149
E('ERR_HTTP_INVALID_HEADER_VALUE',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const http = require('http');
6+
7+
function shouldThrowOnMoreBytes() {
8+
const server = http.createServer(common.mustCall((req, res) => {
9+
res.strictContentLength = true;
10+
res.setHeader('Content-Length', 5);
11+
res.write('hello');
12+
assert.throws(() => {
13+
res.write('a');
14+
}, {
15+
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
16+
});
17+
res.statusCode = 200;
18+
res.end();
19+
}));
20+
21+
server.listen(0, () => {
22+
const req = http.get({
23+
port: server.address().port,
24+
}, common.mustCall((res) => {
25+
res.resume();
26+
assert.strictEqual(res.statusCode, 200);
27+
server.close();
28+
}));
29+
req.end();
30+
});
31+
}
32+
33+
function shouldNotThrow() {
34+
const server = http.createServer(common.mustCall((req, res) => {
35+
res.strictContentLength = true;
36+
res.write('helloaa');
37+
res.statusCode = 200;
38+
res.end('ending');
39+
}));
40+
41+
server.listen(0, () => {
42+
http.get({
43+
port: server.address().port,
44+
}, common.mustCall((res) => {
45+
res.resume();
46+
assert.strictEqual(res.statusCode, 200);
47+
server.close();
48+
}));
49+
});
50+
}
51+
52+
53+
function shouldThrowOnFewerBytes() {
54+
const server = http.createServer(common.mustCall((req, res) => {
55+
res.strictContentLength = true;
56+
res.setHeader('Content-Length', 5);
57+
res.write('a');
58+
res.statusCode = 200;
59+
assert.throws(() => {
60+
res.end();
61+
}, {
62+
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
63+
});
64+
res.end('aaaa');
65+
}));
66+
67+
server.listen(0, () => {
68+
http.get({
69+
port: server.address().port,
70+
}, common.mustCall((res) => {
71+
res.resume();
72+
assert.strictEqual(res.statusCode, 200);
73+
server.close();
74+
}));
75+
});
76+
}
77+
78+
shouldThrowOnMoreBytes();
79+
shouldNotThrow();
80+
shouldThrowOnFewerBytes();

test/parallel/test-http-outgoing-properties.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const OutgoingMessage = http.OutgoingMessage;
4949
msg._implicitHeader = function() {};
5050
assert.strictEqual(msg.writableLength, 0);
5151
msg.write('asd');
52-
assert.strictEqual(msg.writableLength, 7);
52+
assert.strictEqual(msg.writableLength, 3);
5353
}
5454

5555
{

test/parallel/test-http-response-multi-content-length.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function test(server) {
2424
{
2525
const server = http.createServer((req, res) => {
2626
res.setHeader('content-length', [2, 1]);
27-
res.end('ok');
27+
res.end('k');
2828
});
2929

3030
test(server);

0 commit comments

Comments
 (0)