Compare commits

...

8 Commits

Author SHA1 Message Date
Damien Arrachequesne
082b68394f chore(release): socket.io-parser@3.4.4 2026-03-17 15:52:17 +01:00
Damien Arrachequesne
3263ce0f13 ci: init publish workflow 2026-03-17 15:44:42 +01:00
Damien Arrachequesne
719f9ebab0 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 15:41:29 +01:00
Damien Arrachequesne
d256cf1efc ci: init workflow 2026-03-17 15:28:51 +01:00
Damien Arrachequesne
060339a206 chore(release): 3.4.3
Diff: https://github.com/socketio/socket.io-parser/compare/3.4.2...3.4.3
2023-05-22 10:00:23 +02:00
Damien Arrachequesne
2dc3c92622 fix: check the format of the event name
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

History of the isPayloadValid() method:

- added in [78f9fc2](78f9fc2) (v4.0.1, socket.io@3.0.0)
- updated in [1c220dd](1c220dd) (v4.0.4, socket.io@3.1.0)

Backported from 3b78117bf6
2023-05-22 09:56:28 +02:00
Damien Arrachequesne
4b3c191bc4 chore(release): 3.4.2
Diff: https://github.com/socketio/socket.io-parser/compare/3.4.1...3.4.2
2022-11-09 11:18:30 +01:00
Damien Arrachequesne
04d23cecaf 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:04:00 +01:00
10 changed files with 237 additions and 41 deletions

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

@@ -0,0 +1,35 @@
name: CI
on:
push:
branches:
- 'socket.io-parser/3.4.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

@@ -1,20 +0,0 @@
language: node_js
sudo: false
node_js:
- '10'
- '12'
- '14'
git:
depth: 1
matrix:
include:
- node_js: 10
env: BROWSERS=1
env:
global:
- secure: >-
Ea4P/R9UlWzDlHSP5ynmLiD/YgLjecIvCviOcRTle9mV3P1j2k94Ay1LVu1Jw4whlNmWLq2Z/p8M63L92ODPMlarPsuME8HlP4zGr41whFhRbFdda4k3zrHfUhZBlnhY1MVWXTtVm/l7DOzpBrNh+wKecxZB3yyyEaA+PSG3qcQ=
- secure: >-
JmPf38qx5Rb6K+WYOMwb5YmESkDmVJ6tgggiJIuyRfHsgQVOO7XBwZuspIKGTSFolUIMaqwQe79Kd+Ehs2ZZ/0lUyF2/6xW3FqFnASUusYJcZdfRjypmBFWs6BRdtEORM8HL0dgBx4O4u/e4ZvtygumbPahjQbMDaqN+MvlpjD0=
- secure: >-
c3pnLhy3VDJqMl16ABA+8vt3I623aNa2wkLceLXb2V1Dc6eiZeulDH2ekwmdVo/r2WwGIKP3Y6B0mq/xP4W0hg4uT+xWh0AmFHclVyM/yp/AqfXrDUv17Vm0vB7OIgp332OiAlK6Dr13YDbWW8iZxmID41O2+2qohLGPn5JMncg=

View File

@@ -1,3 +1,30 @@
## [3.4.4](https://github.com/socketio/socket.io-parser/compare/3.4.3...3.4.4) (2026-03-17)
### Bug Fixes
* add a limit to the number of binary attachments ([719f9eb](https://github.com/socketio/socket.io/commit/719f9ebab0772ffb882bd614b387e585c1aa75d4))
## [3.4.3](https://github.com/socketio/socket.io-parser/compare/3.4.2...3.4.3) (2023-05-22)
### Bug Fixes
* check the format of the event name ([2dc3c92](https://github.com/socketio/socket.io-parser/commit/2dc3c92622dad113b8676be06f23b1ed46b02ced))
## [3.4.2](https://github.com/socketio/socket.io-parser/compare/3.4.1...3.4.2) (2022-11-09)
### Bug Fixes
* check the format of the index of each attachment ([04d23ce](https://github.com/socketio/socket.io-parser/commit/04d23cecafe1b859fb03e0cbf6ba3b74dff56d14))
## [3.4.1](https://github.com/socketio/socket.io-parser/compare/3.4.0...3.4.1) (2020-05-13)

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);
@@ -269,11 +276,12 @@ Decoder.prototype.add = function(obj) {
* 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 = {
@@ -292,7 +300,13 @@ function decodeString(str) {
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)
@@ -326,11 +340,10 @@ function decodeString(str) {
// 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");
}
}
@@ -346,6 +359,26 @@ function tryParse(str) {
}
}
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);
}
}
/**
* Deallocates a parser's resources
*
@@ -410,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
);
};

View File

@@ -1,10 +1,10 @@
{
"name": "socket.io-parser",
"version": "3.4.1",
"version": "3.4.4",
"description": "socket.io protocol parser",
"repository": {
"type": "git",
"url": "https://github.com/socketio/socket.io-parser.git"
"url": "git+https://github.com/socketio/socket.io.git"
},
"files": [
"binary.js",

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$/);
});
});