diff --git a/packages/socket.io-parser/lib/index.ts b/packages/socket.io-parser/lib/index.ts index 1cb7bb70..324568b1 100644 --- a/packages/socket.io-parser/lib/index.ts +++ b/packages/socket.io-parser/lib/index.ts @@ -135,6 +135,20 @@ interface DecoderReservedEvents { decoded: (packet: Packet) => void; } +type JSONReviver = (this: any, key: string, value: any) => any; + +export interface DecoderOptions { + /** + * Custom reviver to pass down to JSON.parse() + */ + reviver?: JSONReviver; + /** + * Maximum number of binary attachments per packet + * @default 10 + */ + maxAttachments?: number; +} + /** * A socket.io Decoder instance * @@ -142,14 +156,20 @@ interface DecoderReservedEvents { */ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> { private reconstructor: BinaryReconstructor; + private opts: Required; /** * Decoder constructor - * - * @param {function} reviver - custom reviver to pass down to JSON.stringify */ - constructor(private reviver?: (this: any, key: string, value: any) => any) { + constructor(opts?: DecoderOptions | JSONReviver) { super(); + this.opts = Object.assign( + { + reviver: undefined, + maxAttachments: 10, + }, + typeof opts === "function" ? { reviver: opts } : opts, + ); } /** @@ -224,7 +244,13 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> { if (buf != Number(buf) || str.charAt(i) !== "-") { throw new Error("Illegal attachments"); } - p.attachments = Number(buf); + const n = Number(buf); + if (!isInteger(n) || n < 0) { + throw new Error("Illegal attachments"); + } else if (n > this.opts.maxAttachments) { + throw new Error("too many attachments"); + } + p.attachments = n; } // look up namespace (if any) @@ -271,7 +297,7 @@ export class Decoder extends Emitter<{}, {}, DecoderReservedEvents> { private tryParse(str) { try { - return JSON.parse(str, this.reviver); + return JSON.parse(str, this.opts.reviver); } catch (e) { return false; } diff --git a/packages/socket.io-parser/test/parser.js b/packages/socket.io-parser/test/parser.js index 0bbc5716..63e89072 100644 --- a/packages/socket.io-parser/test/parser.js +++ b/packages/socket.io-parser/test/parser.js @@ -107,6 +107,56 @@ describe("socket.io-parser", () => { } }); + 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( @@ -125,6 +175,16 @@ describe("socket.io-parser", () => { 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$/, );