Skip to content

Commit b25738c

Browse files
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 }); } } } }); ```
1 parent f630158 commit b25738c

File tree

2 files changed

+91
-5
lines changed

2 files changed

+91
-5
lines changed

packages/socket.io-parser/lib/index.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,21 +135,41 @@ interface DecoderReservedEvents {
135135
decoded: (packet: Packet) => void;
136136
}
137137

138+
type JSONReviver = (this: any, key: string, value: any) => any;
139+
140+
export interface DecoderOptions {
141+
/**
142+
* Custom reviver to pass down to JSON.parse()
143+
*/
144+
reviver?: JSONReviver;
145+
/**
146+
* Maximum number of binary attachments per packet
147+
* @default 10
148+
*/
149+
maxAttachments?: number;
150+
}
151+
138152
/**
139153
* A socket.io Decoder instance
140154
*
141155
* @return {Object} decoder
142156
*/
143157
export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
144158
private reconstructor: BinaryReconstructor;
159+
private opts: Required<DecoderOptions>;
145160

146161
/**
147162
* Decoder constructor
148-
*
149-
* @param {function} reviver - custom reviver to pass down to JSON.stringify
150163
*/
151-
constructor(private reviver?: (this: any, key: string, value: any) => any) {
164+
constructor(opts?: DecoderOptions | JSONReviver) {
152165
super();
166+
this.opts = Object.assign(
167+
{
168+
reviver: undefined,
169+
maxAttachments: 10,
170+
},
171+
typeof opts === "function" ? { reviver: opts } : opts,
172+
);
153173
}
154174

155175
/**
@@ -224,7 +244,13 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
224244
if (buf != Number(buf) || str.charAt(i) !== "-") {
225245
throw new Error("Illegal attachments");
226246
}
227-
p.attachments = Number(buf);
247+
const n = Number(buf);
248+
if (!isInteger(n) || n < 0) {
249+
throw new Error("Illegal attachments");
250+
} else if (n > this.opts.maxAttachments) {
251+
throw new Error("too many attachments");
252+
}
253+
p.attachments = n;
228254
}
229255

230256
// look up namespace (if any)
@@ -271,7 +297,7 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> {
271297

272298
private tryParse(str) {
273299
try {
274-
return JSON.parse(str, this.reviver);
300+
return JSON.parse(str, this.opts.reviver);
275301
} catch (e) {
276302
return false;
277303
}

packages/socket.io-parser/test/parser.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,56 @@ describe("socket.io-parser", () => {
107107
}
108108
});
109109

110+
it("throws an error when receiving too many attachments", () => {
111+
const decoder = new Decoder({ maxAttachments: 2 });
112+
113+
expect(() => {
114+
decoder.add(
115+
'53-["hello",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1},{"_placeholder":true,"num":2}]',
116+
);
117+
}).to.throwException(/^too many attachments$/);
118+
});
119+
120+
it("decodes with a custom reviver", () => {
121+
const decoder = new Decoder((key, value) => {
122+
if (key === "a") {
123+
return value.toUpperCase();
124+
} else {
125+
return value;
126+
}
127+
});
128+
129+
return new Promise((resolve) => {
130+
decoder.on("decoded", (packet) => {
131+
expect(packet.data).to.eql(["b", { a: "VAL" }]);
132+
resolve();
133+
});
134+
135+
decoder.add('2["b",{"a":"val"}]');
136+
});
137+
});
138+
139+
it("decodes with a custom reviver (options object)", () => {
140+
const decoder = new Decoder({
141+
reviver: (key, value) => {
142+
if (key === "a") {
143+
return value.toUpperCase();
144+
} else {
145+
return value;
146+
}
147+
},
148+
});
149+
150+
return new Promise((resolve) => {
151+
decoder.on("decoded", (packet) => {
152+
expect(packet.data).to.eql(["b", { a: "VAL" }]);
153+
resolve();
154+
});
155+
156+
decoder.add('2["b",{"a":"val"}]');
157+
});
158+
});
159+
110160
it("throw an error upon parsing error", () => {
111161
const isInvalidPayload = (str) =>
112162
expect(() => new Decoder().add(str)).to.throwException(
@@ -125,6 +175,16 @@ describe("socket.io-parser", () => {
125175
isInvalidPayload('2["connect"]');
126176
isInvalidPayload('2["disconnect","123"]');
127177

178+
const isInvalidAttachmentCount = (str) =>
179+
expect(() => new Decoder().add(str)).to.throwException(
180+
/^Illegal attachments$/,
181+
);
182+
183+
isInvalidAttachmentCount("5");
184+
isInvalidAttachmentCount("51");
185+
isInvalidAttachmentCount("5a-");
186+
isInvalidAttachmentCount("51.23-");
187+
128188
expect(() => new Decoder().add("999")).to.throwException(
129189
/^unknown packet type 9$/,
130190
);

0 commit comments

Comments
 (0)