Compare commits

...

13 Commits

Author SHA1 Message Date
Damien Arrachequesne
17bc1d65cf chore(release): socket.io-parser@3.3.5 2026-03-17 16:05:14 +01:00
Damien Arrachequesne
2ba71db5c3 ci: init publish workflow 2026-03-17 16:02:56 +01:00
Damien Arrachequesne
9d39f1f080 fix(parser): add a limit to the number of binary attachments
Backported from main: b25738c416

When a packet contains binary elements, the built-in parser does not modify them and simply sends them in their own WebSocket frame.

Example: `socket.emit("some event", Buffer.of(1,2,3))`

is encoded and transferred as:

- 1st frame: 51-["some event",{"_placeholder":true,"num":0}]
- 2nd frame: <buffer 01 02 03>

where:

- `5` is the type of the packet (binary message)
- `1` is the number of binary attachments
- `-` is the separator
- `["some event",{"_placeholder":true,"num":0}]` is the payload (including the placeholder)

On the receiving end, the parser reads the number of attachments and buffers them until they are all received.

Before this change, the built-in parser accepted any number of binary attachments, which could be exploited to make the server run out of memory.

The number of attachments is now limited to 10, which should be sufficient for most use cases.

The limit can be increased with a custom `parser`:

```js
import { Encoder, Decoder } from "socket.io-parser";

const io = new Server({
  parser: {
    Encoder,
    Decoder: class extends Decoder {
      constructor() {
        super({
          maxAttachments: 20
        });
      }
    }
  }
});
```
2026-03-17 16:00:15 +01:00
Damien Arrachequesne
6b2f875339 ci: init workflow 2026-03-17 16:00:05 +01:00
Damien Arrachequesne
1e9ebc6b7f chore(release): 3.3.4
Diff: https://github.com/Automattic/socket.io-parser/compare/3.3.3...3.3.4
2024-07-22 11:08:17 +02:00
Arnau Fugarolas Barbena
ee00660749 fix: check the format of the event name (#125)
A packet like '2[{"toString":"foo"}]' was decoded as:

{
  type: EVENT,
  data: [ { "toString": "foo" } ]
}

Which would then throw an error when passed to the EventEmitter class:

> TypeError: Cannot convert object to primitive value
>    at Socket.emit (node:events:507:25)
>    at .../node_modules/socket.io/lib/socket.js:531:14

Backported from 3b78117bf6
2024-07-22 11:05:42 +02:00
Damien Arrachequesne
cd11e38e1a chore(release): 3.3.3
Diff: https://github.com/Automattic/socket.io-parser/compare/3.3.2...3.3.3
2022-11-09 11:22:22 +01:00
Damien Arrachequesne
fb21e422fc fix: check the format of the index of each attachment
A specially crafted packet could be incorrectly decoded.

Example:

```js
const decoder = new Decoder();

decoder.on("decoded", (packet) => {
  console.log(packet.data); // prints [ 'hello', [Function: splice] ]
})

decoder.add('51-["hello",{"_placeholder":true,"num":"splice"}]');
decoder.add(Buffer.from("world"));
```

As usual, please remember not to trust user input.

Backported from b5d0cb7dc5
2022-11-09 11:21:34 +01:00
Damien Arrachequesne
3b0a3925fd chore(release): 3.3.2
Diff: https://github.com/Automattic/socket.io-parser/compare/3.3.1...3.3.2
2021-01-09 14:51:19 +01:00
bcaller
89197a05c4 fix: prevent DoS (OOM) via massive packets (#95)
When maxHttpBufferSize is large (1e8 bytes), a payload of length 100MB
can be sent like so:

99999991:422222222222222222222222222222222222222222222...

This massive packet can cause OOM via building up many many
`ConsOneByteString` objects due to concatenation:
99999989 `ConsOneByteString`s and then converting the massive integer to
a `Number`.

The performance can be improved to avoid this by using `substring`
rather than building the string via concatenation.

Below I tried one payload of length 7e7 as the 1e8 payload took so
long to process that it timed out before running out of memory.

```
==== JS stack trace =========================================

    0: ExitFrame [pc: 0x13c5b79]
Security context: 0x152fe7b808d1 <JSObject>
    1: decodeString [0x2dd385fb5d1] [/node_modules/socket.io-parser/index.js:~276] [pc=0xf59746881be](this=0x175d34c42b69 <JSGlobal Object>,0x14eccff10fe1 <Very long string[69999990]>)
    2: add [0x31fc2693da29] [/node_modules/socket.io-parser/index.js:242] [bytecode=0xa7ed6554889 offset=11](this=0x0a2881be5069 <Decoder map = 0x3ceaa8bf48c9>,0x14eccff10fe1 <Very...

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
 1: 0xa09830 node::Abort() [node]
 2: 0xa09c55 node::OnFatalError(char const*, char const*) [node]
 3: 0xb7d71e v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [node]
 4: 0xb7da99 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [node]
 5: 0xd2a1f5  [node]
 6: 0xd2a886 v8::internal::Heap::RecomputeLimits(v8::internal::GarbageCollector) [node]
 7: 0xd37105 v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::GCCallbackFlags) [node]
 8: 0xd37fb5 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 9: 0xd3965f v8::internal::Heap::HandleGCRequest() [node]
10: 0xce8395 v8::internal::StackGuard::HandleInterrupts() [node]
11: 0x1042cb6 v8::internal::Runtime_StackGuard(int, unsigned long*, v8::internal::Isolate*) [node]
12: 0x13c5b79  [node]
```

Backported from master: dcb942d24d
2021-01-09 14:43:12 +01:00
Damien Arrachequesne
25ca624b0d chore(release): 3.3.1
Diff: https://github.com/socketio/socket.io-parser/compare/3.3.0...3.3.1
2020-09-30 02:38:02 +02:00
Damien Arrachequesne
b51b39b78d test: use Node.js 10 for the browser tests
It seems there is something wrong with newer versions (the CI seems
stuck). Let's pin the version for now.
2020-09-30 01:24:44 +02:00
Damien Arrachequesne
4184e46534 chore: bump component-emitter dependency
Subscribing/unsubscribing for a lot of different event types could lead
to a memory leak.

See aa2e57acc7

Diff: https://github.com/component/emitter/compare/1.2.1...1.3.0
2020-09-30 00:26:54 +02:00
11 changed files with 7217 additions and 33 deletions

35
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: CI
on:
push:
branches:
- 'socket.io-parser/3.3.x'
permissions:
contents: read
jobs:
test-node:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version:
- 16
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test

31
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# reference: https://docs.npmjs.com/trusted-publishers#for-github-actions
name: Publish
on:
push:
tags:
- 'socket.io-parser@*'
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Use Node.js 24
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Publish package
run: npm publish --tag v2-latest

View File

@@ -7,7 +7,7 @@ git:
depth: 1
matrix:
include:
- node_js: node
- node_js: 10
env: BROWSERS=1
cache:
directories:

37
CHANGELOG.md Normal file
View File

@@ -0,0 +1,37 @@
## [3.3.5](https://github.com/socketio/socket.io-parser/compare/3.3.4...3.3.5) (2026-03-17)
### Bug Fixes
* add a limit to the number of binary attachments ([9d39f1f](https://github.com/socketio/socket.io/commit/9d39f1f080510f036782f2177fac701cc041faaf))
## [3.3.4](https://github.com/Automattic/socket.io-parser/compare/3.3.3...3.3.4) (2024-07-22)
### Bug Fixes
* check the format of the event name ([#125](https://github.com/Automattic/socket.io-parser/issues/125)) ([ee00660](https://github.com/Automattic/socket.io-parser/commit/ee006607495eca4ec7262ad080dd3a91439a5ba4))
## [3.3.3](https://github.com/Automattic/socket.io-parser/compare/3.3.2...3.3.3) (2022-11-09)
### Bug Fixes
* check the format of the index of each attachment ([fb21e42](https://github.com/Automattic/socket.io-parser/commit/fb21e422fc193b34347395a33e0f625bebc09983))
## [3.3.2](https://github.com/Automattic/socket.io-parser/compare/3.3.1...3.3.2) (2021-01-09)
### Bug Fixes
* prevent DoS (OOM) via massive packets ([#95](https://github.com/Automattic/socket.io-parser/issues/95)) ([89197a0](https://github.com/Automattic/socket.io-parser/commit/89197a05c43b18cc4569fd178d56e7bb8f403865))
## [3.3.1](https://github.com/socketio/socket.io-parser/compare/3.3.0...3.3.1) (2020-09-30)

View File

@@ -70,8 +70,16 @@ exports.reconstructPacket = function(packet, buffers) {
function _reconstructPacket(data, buffers) {
if (!data) return data;
if (data && data._placeholder) {
return buffers[data.num]; // appropriate buffer (should be natural order anyway)
if (data && data._placeholder === true) {
var isIndexValid =
typeof data.num === "number" &&
data.num >= 0 &&
data.num < buffers.length;
if (isIndexValid) {
return buffers[data.num]; // appropriate buffer (should be natural order anyway)
} else {
throw new Error("illegal attachments");
}
} else if (isArray(data)) {
for (var i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers);

View File

@@ -218,8 +218,12 @@ function encodeAsBinary(obj, callback) {
* @api public
*/
function Decoder() {
function Decoder(opts) {
this.reconstructor = null;
opts = opts || {};
this.opts = {
maxAttachments: opts.maxAttachments || 10,
};
}
/**
@@ -239,7 +243,10 @@ Emitter(Decoder.prototype);
Decoder.prototype.add = function(obj) {
var packet;
if (typeof obj === 'string') {
packet = decodeString(obj);
if (this.reconstructor) {
throw new Error("got plaintext data when reconstructing a packet");
}
packet = decodeString(obj, this.opts.maxAttachments);
if (exports.BINARY_EVENT === packet.type || exports.BINARY_ACK === packet.type) { // binary packet's json
this.reconstructor = new BinaryReconstructor(packet);
@@ -265,15 +272,36 @@ Decoder.prototype.add = function(obj) {
}
};
function isPayloadValid(type, payload) {
switch (type) {
case 0: // CONNECT
return typeof payload === "object";
case 1: // DISCONNECT
return payload === undefined;
case 4: // ERROR
return typeof payload === "string" || typeof payload === "object";
case 2: // EVENT
case 5: // BINARY_EVENT
return (
isArray(payload) &&
(typeof payload[0] === "string" || typeof payload[0] === "number")
);
case 3: // ACK
case 6: // BINARY_ACK
return isArray(payload);
}
}
/**
* Decode a packet String (JSON data)
*
* @param {String} str
* @param {Number} maxAttachments - the maximum number of binary attachments
* @return {Object} packet
* @api private
*/
function decodeString(str) {
function decodeString(str, maxAttachments) {
var i = 0;
// look up type
var p = {
@@ -286,26 +314,30 @@ function decodeString(str) {
// look up attachments if type binary
if (exports.BINARY_EVENT === p.type || exports.BINARY_ACK === p.type) {
var buf = '';
while (str.charAt(++i) !== '-') {
buf += str.charAt(i);
if (i == str.length) break;
}
var start = i + 1;
while (str.charAt(++i) !== '-' && i != str.length) {}
var buf = str.substring(start, i);
if (buf != Number(buf) || str.charAt(i) !== '-') {
throw new Error('Illegal attachments');
}
p.attachments = Number(buf);
var n = Number(buf);
if (!isInteger(n) || n < 0) {
throw new Error("Illegal attachments");
} else if (n > maxAttachments) {
throw new Error("too many attachments");
}
p.attachments = n;
}
// look up namespace (if any)
if ('/' === str.charAt(i + 1)) {
p.nsp = '';
var start = i + 1;
while (++i) {
var c = str.charAt(i);
if (',' === c) break;
p.nsp += c;
if (i === str.length) break;
}
p.nsp = str.substring(start, i);
} else {
p.nsp = '/';
}
@@ -313,27 +345,25 @@ function decodeString(str) {
// look up id
var next = str.charAt(i + 1);
if ('' !== next && Number(next) == next) {
p.id = '';
var start = i + 1;
while (++i) {
var c = str.charAt(i);
if (null == c || Number(c) != c) {
--i;
break;
}
p.id += str.charAt(i);
if (i === str.length) break;
}
p.id = Number(p.id);
p.id = Number(str.substring(start, i + 1));
}
// look up json data
if (str.charAt(++i)) {
var payload = tryParse(str.substr(i));
var isPayloadValid = payload !== false && (p.type === exports.ERROR || isArray(payload));
if (isPayloadValid) {
if (isPayloadValid(p.type, payload)) {
p.data = payload;
} else {
return error('invalid payload');
throw new Error("invalid payload");
}
}
@@ -413,3 +443,13 @@ function error(msg) {
data: 'parser error: ' + msg
};
}
var isInteger =
Number.isInteger ||
function (value) {
return (
typeof value === "number" &&
isFinite(value) &&
Math.floor(value) === value
);
};

6961
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "socket.io-parser",
"version": "3.3.0",
"version": "3.3.5",
"description": "socket.io protocol parser",
"repository": {
"type": "git",
"url": "https://github.com/Automattic/socket.io-parser.git"
"url": "git+https://github.com/socketio/socket.io.git"
},
"files": [
"binary.js",
@@ -12,8 +12,8 @@
"is-buffer.js"
],
"dependencies": {
"component-emitter": "~1.3.0",
"debug": "~3.1.0",
"component-emitter": "1.2.1",
"isarray": "2.0.1"
},
"devDependencies": {

View File

@@ -50,7 +50,7 @@ describe('parser', function() {
it('cleans itself up on close', function() {
var packet = {
type: parser.BINARY_EVENT,
data: [new ArrayBuffer(2), new ArrayBuffer(3)],
data: ["foo", new ArrayBuffer(2), new ArrayBuffer(3)],
id: 0,
nsp: '/'
};

View File

@@ -1,8 +1,7 @@
var parser = require('../index.js');
var expect = require('expect.js');
var helpers = require('./helpers.js');
var encode = parser.encode;
var decode = parser.decode;
var Decoder = parser.Decoder;
describe('parser', function() {
it('encodes a Buffer', function() {
@@ -14,6 +13,15 @@ describe('parser', function() {
});
});
it("encodes a nested Buffer", function() {
helpers.test_bin({
type: parser.BINARY_EVENT,
data: ["a", { b: ["c", Buffer.from("abc", "utf8")] }],
id: 23,
nsp: "/cool",
});
});
it('encodes a binary ack with Buffer', function() {
helpers.test_bin({
type: parser.BINARY_ACK,
@@ -22,4 +30,39 @@ describe('parser', function() {
nsp: '/back'
})
});
it("throws an error when adding an attachment with an invalid 'num' attribute (string)", function() {
var decoder = new Decoder();
expect(function() {
decoder.add('51-["hello",{"_placeholder":true,"num":"splice"}]');
decoder.add(Buffer.from("world"));
}).to.throwException(/^illegal attachments$/);
});
it("throws an error when adding an attachment with an invalid 'num' attribute (out-of-bound)", function() {
var decoder = new Decoder();
expect(function() {
decoder.add('51-["hello",{"_placeholder":true,"num":1}]');
decoder.add(Buffer.from("world"));
}).to.throwException(/^illegal attachments$/);
});
it("throws an error when adding an attachment without header", function() {
var decoder = new Decoder();
expect(function() {
decoder.add(Buffer.from("world"));
}).to.throwException(/^got binary data when not reconstructing a packet$/);
});
it("throws an error when decoding a binary event without attachments", function() {
var decoder = new Decoder();
expect(function() {
decoder.add('51-["hello",{"_placeholder":true,"num":0}]');
decoder.add('2["hello"]');
}).to.throwException(/^got plaintext data when reconstructing a packet$/);
});
});

View File

@@ -86,12 +86,41 @@ describe('parser', function(){
}
});
it('returns an error packet on parsing error', function(done){
var decoder = new parser.Decoder();
decoder.on('decoded', function(packet) {
expect(packet).to.eql({ type: 4, data: 'parser error: invalid payload' });
done();
});
decoder.add('442["some","data"');
it('returns an error packet on parsing error', function(){
function isInvalidPayload (str) {
expect(function () {
new parser.Decoder().add(str)
}).to.throwException(/^invalid payload$/);
}
isInvalidPayload('442["some","data"');
isInvalidPayload('0/admin,"invalid"');
isInvalidPayload("1/admin,{}");
isInvalidPayload('2/admin,"invalid');
isInvalidPayload("2/admin,{}");
isInvalidPayload('2[{"toString":"foo"}]');
isInvalidPayload('2[true,"foo"]');
isInvalidPayload('2[null,"bar"]');
function isInvalidAttachmentCount (str) {
expect(() => new parser.Decoder().add(str)).to.throwException(
/^Illegal attachments$/,
);
}
isInvalidAttachmentCount("5");
isInvalidAttachmentCount("51");
isInvalidAttachmentCount("5a-");
isInvalidAttachmentCount("51.23-");
});
it("throws an error when receiving too many attachments", () => {
const decoder = new parser.Decoder({ maxAttachments: 2 });
expect(() => {
decoder.add(
'53-["hello",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1},{"_placeholder":true,"num":2}]',
);
}).to.throwException(/^too many attachments$/);
});
});