From 59c7767bd86ffe79f1812b8bab403bca1bf63351 Mon Sep 17 00:00:00 2001 From: junsj119 Date: Fri, 7 Aug 2026 11:37:56 +0900 Subject: [PATCH] fix: avoid stack overflow in hasBinary() for objects with a toJSON() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hasBinary() recursed into all own enumerable keys before checking for a custom toJSON() method. Objects that define toJSON() but also hold internal circular references — most notably Mongoose documents, whose `$__` cache points back to the document — overflowed the stack before the toJSON() short-circuit was ever reached. In sharded mode the resulting RangeError propagates through encode() -> doPublish() -> broadcast() and the event is dropped without any error surfaced (unless DEBUG is enabled). Check toJSON() before the key traversal, mirroring how the payload is actually serialized, so such objects are inspected through their toJSON() representation instead of their internal fields. Fixes #572 --- lib/util.ts | 10 ++++++---- test/hasBinary.ts | 33 +++++++++++++++++++++++++++++++++ test/test-runner.ts | 1 + 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 test/hasBinary.ts diff --git a/lib/util.ts b/lib/util.ts index cc4f74b..c1c48de 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -16,16 +16,18 @@ export function hasBinary(obj: any, toJSON?: boolean): boolean { return false; } + // check toJSON() before the own keys: Mongoose documents and the like hold + // circular internals (e.g. `$__`) that would otherwise overflow the stack. See #572 + if (obj.toJSON && typeof obj.toJSON === "function" && !toJSON) { + return hasBinary(obj.toJSON(), true); + } + for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) { return true; } } - if (obj.toJSON && typeof obj.toJSON === "function" && !toJSON) { - return hasBinary(obj.toJSON(), true); - } - return false; } diff --git a/test/hasBinary.ts b/test/hasBinary.ts new file mode 100644 index 0000000..44bb22e --- /dev/null +++ b/test/hasBinary.ts @@ -0,0 +1,33 @@ +import expect = require("expect.js"); +import { hasBinary } from "../lib/util"; + +describe("hasBinary", () => { + it("returns true when the payload contains binary data", () => { + expect(hasBinary(new Uint8Array([1, 2, 3]))).to.be(true); + expect(hasBinary({ a: { b: new ArrayBuffer(4) } })).to.be(true); + expect(hasBinary(["a", new Uint8Array(1)])).to.be(true); + }); + + it("returns false when the payload does not contain binary data", () => { + expect(hasBinary({ a: 1, b: "two", c: [3, 4] })).to.be(false); + expect(hasBinary(null)).to.be(false); + expect(hasBinary("string")).to.be(false); + }); + + it("does not overflow the stack on objects with circular references and a toJSON() method (e.g. Mongoose documents)", () => { + const doc: any = { name: "test" }; + doc.$__ = { document: doc }; // circular internal reference, like Mongoose's $__ + doc.toJSON = () => ({ name: "test" }); + + expect(() => hasBinary(doc)).to.not.throwException(); + expect(hasBinary(doc)).to.be(false); + }); + + it("detects binary through the toJSON() representation", () => { + const doc: any = { name: "test" }; + doc.$__ = { document: doc }; // circular internal reference + doc.toJSON = () => ({ name: "test", avatar: new Uint8Array([1, 2]) }); + + expect(hasBinary(doc)).to.be(true); + }); +}); diff --git a/test/test-runner.ts b/test/test-runner.ts index 1e4d566..8490b23 100644 --- a/test/test-runner.ts +++ b/test/test-runner.ts @@ -280,4 +280,5 @@ describe("@socket.io/redis-adapter", () => { )); import("./custom-parser"); + import("./hasBinary"); });