VYPR
Moderate severityOSV Advisory· Published Jan 26, 2026· Updated Jan 26, 2026

CVE-2025-50537

CVE-2025-50537

Description

Stack overflow vulnerability in eslint before 9.26.0 when serializing objects with circular references in eslint/lib/shared/serialization.js. The exploit is triggered via the RuleTester.run() method, which validates test cases and checks for duplicates. During validation, the internal function checkDuplicateTestCase() is called, which in turn uses the isSerializable() function for serialization checks. When a circular reference object is passed in, isSerializable() enters infinite recursion, ultimately causing a stack overflow.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
eslintnpm
< 9.26.09.26.0

Affected products

1

Patches

1
d683aebc8e07

fix: don't crash on tests with circular references in `RuleTester` (#19664)

https://github.com/eslint/eslintMilos DjermanovicApr 28, 2025via ghsa
4 files changed · +238 9
  • lib/rule-tester/rule-tester.js+11 3 modified
    @@ -192,16 +192,24 @@ function cloneDeeplyExcludesParent(x) {
     /**
      * Freezes a given value deeply.
      * @param {any} x A value to freeze.
    + * @param {Set<Object>} seenObjects Objects already seen during the traversal.
      * @returns {void}
      */
    -function freezeDeeply(x) {
    +function freezeDeeply(x, seenObjects = new Set()) {
     	if (typeof x === "object" && x !== null) {
    +		if (seenObjects.has(x)) {
    +			return; // skip to avoid infinite recursion
    +		}
    +		seenObjects.add(x);
    +
     		if (Array.isArray(x)) {
    -			x.forEach(freezeDeeply);
    +			x.forEach(element => {
    +				freezeDeeply(element, seenObjects);
    +			});
     		} else {
     			for (const key in x) {
     				if (key !== "parent" && hasOwnProperty(x, key)) {
    -					freezeDeeply(x[key]);
    +					freezeDeeply(x[key], seenObjects);
     				}
     			}
     		}
    
  • lib/shared/serialization.js+29 6 modified
    @@ -26,21 +26,44 @@ function isSerializablePrimitiveOrPlainObject(val) {
      * Check if a value is serializable.
      * Functions or objects like RegExp cannot be serialized by JSON.stringify().
      * Inspired by: https://stackoverflow.com/questions/30579940/reliable-way-to-check-if-objects-is-serializable-in-javascript
    - * @param {any} val the value
    - * @returns {boolean} true if the value is serializable
    + * @param {any} val The value
    + * @param {Set<Object>} seenObjects Objects already seen in this path from the root object.
    + * @returns {boolean} `true` if the value is serializable
      */
    -function isSerializable(val) {
    +function isSerializable(val, seenObjects = new Set()) {
     	if (!isSerializablePrimitiveOrPlainObject(val)) {
     		return false;
     	}
    -	if (typeof val === "object") {
    +	if (typeof val === "object" && val !== null) {
    +		if (seenObjects.has(val)) {
    +			/*
    +			 * Since this is a depth-first traversal, encountering
    +			 * the same object again means there is a circular reference.
    +			 * Objects with circular references are not serializable.
    +			 */
    +			return false;
    +		}
     		for (const property in val) {
     			if (Object.hasOwn(val, property)) {
     				if (!isSerializablePrimitiveOrPlainObject(val[property])) {
     					return false;
     				}
    -				if (typeof val[property] === "object") {
    -					if (!isSerializable(val[property])) {
    +				if (
    +					typeof val[property] === "object" &&
    +					val[property] !== null
    +				) {
    +					if (
    +						/*
    +						 * We're creating a new Set of seen objects because we want to
    +						 * ensure that `val` doesn't appear again in this path, but it can appear
    +						 * in other paths. This allows for resuing objects in the graph, as long as
    +						 * there are no cycles.
    +						 */
    +						!isSerializable(
    +							val[property],
    +							new Set([...seenObjects, val]),
    +						)
    +					) {
     						return false;
     					}
     				}
    
  • tests/lib/rule-tester/rule-tester.js+172 0 modified
    @@ -4565,6 +4565,79 @@ describe("RuleTester", () => {
     				}, "detected duplicate test case");
     			});
     
    +			it("throws with duplicate object test cases when they are the same object", () => {
    +				const test = { code: "foo" };
    +				assert.throws(() => {
    +					ruleTester.run(
    +						"foo",
    +						{
    +							meta: {},
    +							create() {
    +								return {};
    +							},
    +						},
    +						{
    +							valid: [test, test],
    +							invalid: [],
    +						},
    +					);
    +				}, "detected duplicate test case");
    +			});
    +
    +			it("throws with duplicate object test cases that have multiple references to the same object", () => {
    +				const obj1 = { foo: { bar: "baz" } };
    +				const obj2 = { foo: { bar: "baz" } };
    +
    +				assert.throws(() => {
    +					ruleTester.run(
    +						"foo",
    +						{
    +							meta: {},
    +							create() {
    +								return {};
    +							},
    +						},
    +						{
    +							valid: [
    +								{
    +									code: "foo",
    +									settings: { qux: obj1, quux: obj1 },
    +								},
    +								{
    +									code: "foo",
    +									settings: { qux: obj2, quux: obj2 },
    +								},
    +							],
    +							invalid: [],
    +						},
    +					);
    +				}, "detected duplicate test case");
    +			});
    +
    +			it("does not throw with duplicate object test cases that have circular references", () => {
    +				const obj1 = { foo: "bar" };
    +				obj1.circular = obj1;
    +				const obj2 = { foo: "bar" };
    +				obj2.circular = obj2;
    +
    +				ruleTester.run(
    +					"foo",
    +					{
    +						meta: {},
    +						create() {
    +							return {};
    +						},
    +					},
    +					{
    +						valid: [
    +							{ code: "foo", settings: { baz: obj1 } },
    +							{ code: "foo", settings: { baz: obj2 } },
    +						],
    +						invalid: [],
    +					},
    +				);
    +			});
    +
     			it("throws with string and object test cases", () => {
     				assert.throws(() => {
     					ruleTester.run(
    @@ -4683,6 +4756,105 @@ describe("RuleTester", () => {
     				}, "detected duplicate test case");
     			});
     
    +			it("throws with duplicate object test cases when they are the same object", () => {
    +				const test = {
    +					code: "const x = 123;",
    +					errors: [{ message: "foo bar" }],
    +				};
    +
    +				assert.throws(() => {
    +					ruleTester.run(
    +						"foo",
    +						{
    +							meta: {},
    +							create(context) {
    +								return {
    +									VariableDeclaration(node) {
    +										context.report(node, "foo bar");
    +									},
    +								};
    +							},
    +						},
    +						{
    +							valid: ["foo"],
    +							invalid: [test, test],
    +						},
    +					);
    +				}, "detected duplicate test case");
    +			});
    +
    +			it("throws with duplicate object test cases that have multiple references to the same object", () => {
    +				const obj1 = { foo: { bar: "baz" } };
    +				const obj2 = { foo: { bar: "baz" } };
    +
    +				assert.throws(() => {
    +					ruleTester.run(
    +						"foo",
    +						{
    +							meta: {},
    +							create(context) {
    +								return {
    +									VariableDeclaration(node) {
    +										context.report(node, "foo bar");
    +									},
    +								};
    +							},
    +						},
    +						{
    +							valid: ["foo"],
    +							invalid: [
    +								{
    +									code: "const x = 123;",
    +									settings: { qux: obj1, quux: obj1 },
    +									errors: [{ message: "foo bar" }],
    +								},
    +								{
    +									code: "const x = 123;",
    +									settings: { qux: obj2, quux: obj2 },
    +									errors: [{ message: "foo bar" }],
    +								},
    +							],
    +						},
    +					);
    +				}, "detected duplicate test case");
    +			});
    +
    +			it("does not throw with duplicate object test cases that have circular references", () => {
    +				const obj1 = { foo: "bar" };
    +				obj1.circular = obj1;
    +				const obj2 = { foo: "bar" };
    +				obj2.circular = obj2;
    +
    +				ruleTester.run(
    +					"foo",
    +					{
    +						meta: {},
    +						create(context) {
    +							return {
    +								VariableDeclaration(node) {
    +									context.report(node, "foo bar");
    +								},
    +							};
    +						},
    +					},
    +					{
    +						valid: ["foo"],
    +						invalid: [
    +							{
    +								code: "const x = 123;",
    +								settings: { baz: obj1 },
    +								errors: [{ message: "foo bar" }],
    +							},
    +							{
    +								code: "const x = 123;",
    +								settings: { baz: obj2 },
    +								errors: [{ message: "foo bar" }],
    +							},
    +						],
    +					},
    +				);
    +			});
    +
     			it("throws with duplicate object test cases when options is a primitive", () => {
     				assert.throws(() => {
     					ruleTester.run(
    
  • tests/lib/shared/serialization.js+26 0 modified
    @@ -68,6 +68,32 @@ describe("serialization", () => {
     				assert.isFalse(isSerializable({ a: /abc/u }));
     				assert.isFalse(isSerializable({ a: { b: /abc/u } }));
     			});
    +
    +			it("circular references", () => {
    +				const obj1 = {};
    +				obj1.circular = obj1;
    +				assert.isFalse(isSerializable(obj1));
    +				assert.isFalse(isSerializable({ a: obj1 }));
    +
    +				const obj2 = {};
    +				obj2.a = { circular: obj2 };
    +				assert.isFalse(isSerializable(obj2));
    +				assert.isFalse(isSerializable({ b: obj2 }));
    +
    +				const obj3 = { foo: { bar: "baz" } };
    +				assert.isTrue(
    +					isSerializable({
    +						a: obj3,
    +						b: obj3,
    +						c: {
    +							d: obj3,
    +							e: {
    +								f: obj3,
    +							},
    +						},
    +					}),
    +				);
    +			});
     		});
     
     		describe("array", () => {
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.