Files
socket.io/packages/socket.io-parser/test/parser.js
Damien Arrachequesne b25738c416 fix(parser): add a limit to the number of binary attachments
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 10:57:13 +01:00

301 lines
6.5 KiB
JavaScript

const { PacketType, Decoder, Encoder, isPacketValid } = require("..");
const expect = require("expect.js");
const helpers = require("./helpers.js");
describe("socket.io-parser", () => {
it("exposes types", () => {
expect(PacketType.CONNECT).to.be.a("number");
expect(PacketType.DISCONNECT).to.be.a("number");
expect(PacketType.EVENT).to.be.a("number");
expect(PacketType.ACK).to.be.a("number");
expect(PacketType.CONNECT_ERROR).to.be.a("number");
expect(PacketType.BINARY_EVENT).to.be.a("number");
expect(PacketType.BINARY_ACK).to.be.a("number");
});
it("encodes connection", () => {
return helpers.test({
type: PacketType.CONNECT,
nsp: "/woot",
data: {
token: "123",
},
});
});
it("encodes disconnection", () => {
return helpers.test({
type: PacketType.DISCONNECT,
nsp: "/woot",
});
});
it("encodes an event", () => {
return helpers.test({
type: PacketType.EVENT,
data: ["a", 1, {}],
nsp: "/",
});
});
it("encodes an event (with an integer as event name)", () => {
return helpers.test({
type: PacketType.EVENT,
data: [1, "a", {}],
nsp: "/",
});
});
it("encodes an event (with ack)", () => {
return helpers.test({
type: PacketType.EVENT,
data: ["a", 1, {}],
id: 1,
nsp: "/test",
});
});
it("encodes an ack", () => {
return helpers.test({
type: PacketType.ACK,
data: ["a", 1, {}],
id: 123,
nsp: "/",
});
});
it("encodes an connect error", () => {
return helpers.test({
type: PacketType.CONNECT_ERROR,
data: "Unauthorized",
nsp: "/",
});
});
it("encodes an connect error (with object)", () => {
return helpers.test({
type: PacketType.CONNECT_ERROR,
data: {
message: "Unauthorized",
},
nsp: "/",
});
});
it("throws an error when encoding circular objects", () => {
const a = {};
a.b = a;
const data = {
type: PacketType.EVENT,
data: a,
id: 1,
nsp: "/",
};
const encoder = new Encoder();
expect(() => encoder.encode(data)).to.throwException();
});
it("decodes a bad binary packet", () => {
try {
const decoder = new Decoder();
decoder.add("5");
} catch (e) {
expect(e.message).to.match(/Illegal/);
}
});
it("throws an error when receiving too many attachments", () => {
const decoder = new 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$/);
});
it("decodes with a custom reviver", () => {
const decoder = new Decoder((key, value) => {
if (key === "a") {
return value.toUpperCase();
} else {
return value;
}
});
return new Promise((resolve) => {
decoder.on("decoded", (packet) => {
expect(packet.data).to.eql(["b", { a: "VAL" }]);
resolve();
});
decoder.add('2["b",{"a":"val"}]');
});
});
it("decodes with a custom reviver (options object)", () => {
const decoder = new Decoder({
reviver: (key, value) => {
if (key === "a") {
return value.toUpperCase();
} else {
return value;
}
},
});
return new Promise((resolve) => {
decoder.on("decoded", (packet) => {
expect(packet.data).to.eql(["b", { a: "VAL" }]);
resolve();
});
decoder.add('2["b",{"a":"val"}]');
});
});
it("throw an error upon parsing error", () => {
const isInvalidPayload = (str) =>
expect(() => new Decoder().add(str)).to.throwException(
/^invalid payload$/,
);
isInvalidPayload('442["some","data"');
isInvalidPayload('0/admin,"invalid"');
isInvalidPayload("0[]");
isInvalidPayload("1/admin,{}");
isInvalidPayload('2/admin,"invalid');
isInvalidPayload("2/admin,{}");
isInvalidPayload('2[{"toString":"foo"}]');
isInvalidPayload('2[true,"foo"]');
isInvalidPayload('2[null,"bar"]');
isInvalidPayload('2["connect"]');
isInvalidPayload('2["disconnect","123"]');
const isInvalidAttachmentCount = (str) =>
expect(() => new Decoder().add(str)).to.throwException(
/^Illegal attachments$/,
);
isInvalidAttachmentCount("5");
isInvalidAttachmentCount("51");
isInvalidAttachmentCount("5a-");
isInvalidAttachmentCount("51.23-");
expect(() => new Decoder().add("999")).to.throwException(
/^unknown packet type 9$/,
);
expect(() => new Decoder().add(999)).to.throwException(
/^Unknown type: 999$/,
);
});
it("should resume decoding after calling destroy()", () => {
return new Promise((resolve) => {
const decoder = new Decoder();
decoder.on("decoded", (packet) => {
expect(packet.data).to.eql(["hello"]);
resolve();
});
decoder.add('51-["hello"]');
decoder.destroy();
decoder.add('2["hello"]');
});
});
it("should ensure that a packet is valid", () => {
expect(
isPacketValid({
type: 0,
nsp: "/",
}),
).to.eql(true);
expect(
isPacketValid({
type: 0,
nsp: "/admin",
data: "invalid",
}),
).to.eql(false);
expect(
isPacketValid({
type: 0,
nsp: "/",
data: [],
}),
).to.eql(false);
expect(
isPacketValid({
type: 1,
nsp: "/admin",
data: {},
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/admin",
data: "invalid",
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/admin",
data: {},
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/",
data: { toString: "foo" },
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/",
data: [true, "foo"],
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/",
data: [null, "bar"],
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/",
data: ["connect"],
}),
).to.eql(false);
expect(
isPacketValid({
type: 2,
nsp: "/",
data: ["disconnect", "123"],
}),
).to.eql(false);
});
});