Skip to content

Commit 2b2abd4

Browse files
Nadav0077lpinca
authored andcommitted
[security] Limit retained message parts
Previously, the receiver could retain one `Buffer` entry per buffered chunk or message fragment until enough data was parsed or the message completed. A peer could use many tiny fragments/chunks and make retained memory scale with retained part count rather than message payload size. Add configurable `maxBufferedChunks` and `maxFragments` options to bound the number of retained parts. When either limit is exceeded, emit a `WS_ERR_TOO_MANY_BUFFERED_PARTS` error and close the connection with close code 1008. Signed-off-by: Nadav0077 <18245584+Nadav0077@users.noreply.github.com>
1 parent 78eabe2 commit 2b2abd4

7 files changed

Lines changed: 189 additions & 2 deletions

File tree

doc/ws.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
- [WS_ERR_UNEXPECTED_MASK](#ws_err_unexpected_mask)
6363
- [WS_ERR_UNEXPECTED_RSV_1](#ws_err_unexpected_rsv_1)
6464
- [WS_ERR_UNEXPECTED_RSV_2_3](#ws_err_unexpected_rsv_2_3)
65+
- [WS_ERR_TOO_MANY_BUFFERED_PARTS](#ws_err_too_many_buffered_parts)
6566
- [WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH](#ws_err_unsupported_data_payload_length)
6667
- [WS_ERR_UNSUPPORTED_MESSAGE_LENGTH](#ws_err_unsupported_message_length)
6768

@@ -86,6 +87,10 @@ This class represents a WebSocket server. It extends the `EventEmitter`.
8687
- `handleProtocols` {Function} A function which can be used to handle the
8788
WebSocket subprotocols. See description below.
8889
- `host` {String} The hostname where to bind the server.
90+
- `maxBufferedChunks` {Number} The maximum number of buffered data chunks.
91+
Defaults to 1048576. Set to 0 to disable the limit.
92+
- `maxFragments` {Number} The maximum number of fragments in a message.
93+
Defaults to 131072. Set to 0 to disable the limit.
8994
- `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to
9095
100 MiB (104857600 bytes).
9196
- `noServer` {Boolean} Enable no server mode.
@@ -320,6 +325,10 @@ This class represents a WebSocket. It extends the `EventEmitter`.
320325
cryptographically strong random bytes.
321326
- `handshakeTimeout` {Number} Timeout in milliseconds for the handshake
322327
request. This is reset after every redirection.
328+
- `maxBufferedChunks` {Number} The maximum number of buffered data chunks.
329+
Defaults to 1048576. Set to 0 to disable the limit.
330+
- `maxFragments` {Number} The maximum number of fragments in a message.
331+
Defaults to 131072. Set to 0 to disable the limit.
323332
- `maxPayload` {Number} The maximum allowed message size in bytes. Defaults to
324333
100 MiB (104857600 bytes).
325334
- `maxRedirects` {Number} The maximum number of redirects allowed. Defaults
@@ -690,6 +699,11 @@ A WebSocket frame was received with the RSV1 bit set unexpectedly.
690699

691700
A WebSocket frame was received with the RSV2 or RSV3 bit set unexpectedly.
692701

702+
### WS_ERR_TOO_MANY_BUFFERED_PARTS
703+
704+
The configured maximum number of buffered data chunks or message fragments was
705+
exceeded.
706+
693707
### WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH
694708

695709
A data frame was received with a length longer than the max supported length

lib/receiver.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class Receiver extends Writable {
4040
* extensions
4141
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
4242
* client or server mode
43+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
44+
* buffered data chunks
45+
* @param {Number} [options.maxFragments=0] The maximum number of message
46+
* fragments
4347
* @param {Number} [options.maxPayload=0] The maximum allowed message length
4448
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
4549
* not to skip UTF-8 validation for text and close messages
@@ -54,6 +58,8 @@ class Receiver extends Writable {
5458
this._binaryType = options.binaryType || BINARY_TYPES[0];
5559
this._extensions = options.extensions || {};
5660
this._isServer = !!options.isServer;
61+
this._maxBufferedChunks = options.maxBufferedChunks | 0;
62+
this._maxFragments = options.maxFragments | 0;
5763
this._maxPayload = options.maxPayload | 0;
5864
this._skipUTF8Validation = !!options.skipUTF8Validation;
5965
this[kWebSocket] = undefined;
@@ -89,6 +95,22 @@ class Receiver extends Writable {
8995
_write(chunk, encoding, cb) {
9096
if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
9197

98+
if (
99+
this._maxBufferedChunks > 0 &&
100+
this._buffers.length >= this._maxBufferedChunks
101+
) {
102+
cb(
103+
this.createError(
104+
RangeError,
105+
'Too many buffered chunks',
106+
false,
107+
1008,
108+
'WS_ERR_TOO_MANY_BUFFERED_PARTS'
109+
)
110+
);
111+
return;
112+
}
113+
92114
this._bufferedBytes += chunk.length;
93115
this._buffers.push(chunk);
94116
this.startLoop(cb);
@@ -485,6 +507,22 @@ class Receiver extends Writable {
485507
}
486508

487509
if (data.length) {
510+
if (
511+
this._maxFragments > 0 &&
512+
this._fragments.length >= this._maxFragments
513+
) {
514+
const error = this.createError(
515+
RangeError,
516+
'Too many message fragments',
517+
false,
518+
1008,
519+
'WS_ERR_TOO_MANY_BUFFERED_PARTS'
520+
);
521+
522+
cb(error);
523+
return;
524+
}
525+
488526
//
489527
// This message is not compressed so its length is the sum of the payload
490528
// length of all fragments.
@@ -524,6 +562,22 @@ class Receiver extends Writable {
524562
return;
525563
}
526564

565+
if (
566+
this._maxFragments > 0 &&
567+
this._fragments.length >= this._maxFragments
568+
) {
569+
const error = this.createError(
570+
RangeError,
571+
'Too many message fragments',
572+
false,
573+
1008,
574+
'WS_ERR_TOO_MANY_BUFFERED_PARTS'
575+
);
576+
577+
cb(error);
578+
return;
579+
}
580+
527581
this._fragments.push(buf);
528582
}
529583

lib/websocket-server.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter {
4343
* called
4444
* @param {Function} [options.handleProtocols] A hook to handle protocols
4545
* @param {String} [options.host] The hostname where to bind the server
46+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
47+
* buffered data chunks
48+
* @param {Number} [options.maxFragments=131072] The maximum number of message
49+
* fragments
4650
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
4751
* size
4852
* @param {Boolean} [options.noServer=false] Enable no server mode
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
6569
options = {
6670
allowSynchronousEvents: true,
6771
autoPong: true,
72+
maxBufferedChunks: 1024 * 1024,
73+
maxFragments: 128 * 1024,
6874
maxPayload: 100 * 1024 * 1024,
6975
skipUTF8Validation: false,
7076
perMessageDeflate: false,
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
424430

425431
ws.setSocket(socket, head, {
426432
allowSynchronousEvents: this.options.allowSynchronousEvents,
433+
maxBufferedChunks: this.options.maxBufferedChunks,
434+
maxFragments: this.options.maxFragments,
427435
maxPayload: this.options.maxPayload,
428436
skipUTF8Validation: this.options.skipUTF8Validation
429437
});

lib/websocket.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
201201
* multiple times in the same tick
202202
* @param {Function} [options.generateMask] The function used to generate the
203203
* masking key
204+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
205+
* buffered data chunks
206+
* @param {Number} [options.maxFragments=0] The maximum number of message
207+
* fragments
204208
* @param {Number} [options.maxPayload=0] The maximum allowed message size
205209
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
206210
* not to skip UTF-8 validation for text and close messages
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
212216
binaryType: this.binaryType,
213217
extensions: this._extensions,
214218
isServer: this._isServer,
219+
maxBufferedChunks: options.maxBufferedChunks,
220+
maxFragments: options.maxFragments,
215221
maxPayload: options.maxPayload,
216222
skipUTF8Validation: options.skipUTF8Validation
217223
});
@@ -640,6 +646,10 @@ module.exports = WebSocket;
640646
* masking key
641647
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
642648
* handshake request
649+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
650+
* buffered data chunks
651+
* @param {Number} [options.maxFragments=131072] The maximum number of message
652+
* fragments
643653
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
644654
* size
645655
* @param {Number} [options.maxRedirects=10] The maximum number of redirects
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
660670
autoPong: true,
661671
closeTimeout: CLOSE_TIMEOUT,
662672
protocolVersion: protocolVersions[1],
673+
maxBufferedChunks: 1024 * 1024,
674+
maxFragments: 128 * 1024,
663675
maxPayload: 100 * 1024 * 1024,
664676
skipUTF8Validation: false,
665677
perMessageDeflate: true,
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
10171029
websocket.setSocket(socket, head, {
10181030
allowSynchronousEvents: opts.allowSynchronousEvents,
10191031
generateMask: opts.generateMask,
1032+
maxBufferedChunks: opts.maxBufferedChunks,
1033+
maxFragments: opts.maxFragments,
10201034
maxPayload: opts.maxPayload,
10211035
skipUTF8Validation: opts.skipUTF8Validation
10221036
});

test/receiver.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -988,6 +988,85 @@ describe('Receiver', () => {
988988
});
989989
});
990990

991+
it('emits an error if there are too many message fragments (1/2)', (done) => {
992+
const receiver = new Receiver({ maxFragments: 2 });
993+
994+
receiver.on('error', (err) => {
995+
assert.ok(err instanceof RangeError);
996+
assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS');
997+
assert.strictEqual(err.message, 'Too many message fragments');
998+
assert.strictEqual(err[kStatusCode], 1008);
999+
done();
1000+
});
1001+
1002+
receiver.write(
1003+
Buffer.from([
1004+
0x02,
1005+
0x01,
1006+
0x61, // First non-final binary fragment.
1007+
0x00,
1008+
0x01,
1009+
0x62, // Continuation fragment.
1010+
0x00,
1011+
0x01,
1012+
0x63 // Continuation fragment that exceeds the limit.
1013+
])
1014+
);
1015+
});
1016+
1017+
it('emits an error if there are too many message fragments (2/2)', (done) => {
1018+
const perMessageDeflate = new PerMessageDeflate();
1019+
perMessageDeflate.accept([{}]);
1020+
1021+
const receiver = new Receiver({
1022+
extensions: {
1023+
'permessage-deflate': perMessageDeflate
1024+
},
1025+
maxFragments: 1
1026+
});
1027+
const fragment1 = Buffer.from('foo');
1028+
const fragment2 = Buffer.from('bar');
1029+
1030+
receiver.on('error', (err) => {
1031+
assert.ok(err instanceof RangeError);
1032+
assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS');
1033+
assert.strictEqual(err.message, 'Too many message fragments');
1034+
assert.strictEqual(err[kStatusCode], 1008);
1035+
done();
1036+
});
1037+
1038+
perMessageDeflate.compress(fragment1, false, (err, data) => {
1039+
if (err) return done(err);
1040+
1041+
receiver.write(Buffer.from([0x41, data.length]));
1042+
receiver.write(data);
1043+
1044+
perMessageDeflate.compress(fragment2, true, (err, data) => {
1045+
if (err) return done(err);
1046+
1047+
receiver.write(Buffer.from([0x80, data.length]));
1048+
receiver.write(data);
1049+
});
1050+
});
1051+
});
1052+
1053+
it('emits an error if there are too many buffered chunks', (done) => {
1054+
const receiver = new Receiver({ maxBufferedChunks: 2 });
1055+
1056+
receiver.on('error', (err) => {
1057+
assert.ok(err instanceof RangeError);
1058+
assert.strictEqual(err.code, 'WS_ERR_TOO_MANY_BUFFERED_PARTS');
1059+
assert.strictEqual(err.message, 'Too many buffered chunks');
1060+
assert.strictEqual(err[kStatusCode], 1008);
1061+
done();
1062+
});
1063+
1064+
receiver.write(Buffer.from([0x82, 0x05]));
1065+
receiver.write(Buffer.from([0x61]));
1066+
receiver.write(Buffer.from([0x62]));
1067+
receiver.write(Buffer.from([0x63]));
1068+
});
1069+
9911070
it("honors the 'nodebuffer' binary type", (done) => {
9921071
const receiver = new Receiver();
9931072
const frags = [

test/websocket-server.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,15 @@ describe('WebSocketServer', () => {
6666
});
6767
});
6868

69-
it('accepts the `maxPayload` option', (done) => {
69+
it('accepts the receiver limit options', (done) => {
70+
const maxBufferedChunks = 1024;
71+
const maxFragments = 512;
7072
const maxPayload = 20480;
7173
const wss = new WebSocket.Server(
7274
{
7375
perMessageDeflate: true,
76+
maxBufferedChunks,
77+
maxFragments,
7478
maxPayload,
7579
port: 0
7680
},
@@ -82,6 +86,11 @@ describe('WebSocketServer', () => {
8286
);
8387

8488
wss.on('connection', (ws) => {
89+
assert.strictEqual(
90+
ws._receiver._maxBufferedChunks,
91+
maxBufferedChunks
92+
);
93+
assert.strictEqual(ws._receiver._maxFragments, maxFragments);
8594
assert.strictEqual(ws._receiver._maxPayload, maxPayload);
8695
assert.strictEqual(
8796
ws._receiver._extensions['permessage-deflate']._maxPayload,

test/websocket.test.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ describe('WebSocket', () => {
135135
assert.strictEqual(count, 2);
136136
});
137137

138-
it('accepts the `maxPayload` option', (done) => {
138+
it('accepts the receiver limit options', (done) => {
139+
const maxBufferedChunks = 1024;
140+
const maxFragments = 512;
139141
const maxPayload = 20480;
140142
const wss = new WebSocket.Server(
141143
{
@@ -145,10 +147,17 @@ describe('WebSocket', () => {
145147
() => {
146148
const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
147149
perMessageDeflate: true,
150+
maxBufferedChunks,
151+
maxFragments,
148152
maxPayload
149153
});
150154

151155
ws.on('open', () => {
156+
assert.strictEqual(
157+
ws._receiver._maxBufferedChunks,
158+
maxBufferedChunks
159+
);
160+
assert.strictEqual(ws._receiver._maxFragments, maxFragments);
152161
assert.strictEqual(ws._receiver._maxPayload, maxPayload);
153162
assert.strictEqual(
154163
ws._receiver._extensions['permessage-deflate']._maxPayload,

0 commit comments

Comments
 (0)