CVE-2026-27601
Description
Underscore.js is a utility-belt library for JavaScript. Prior to 1.13.8, the _.flatten and _.isEqual functions use recursion without a depth limit. Under very specific conditions, detailed below, an attacker could exploit this in a Denial of Service (DoS) attack by triggering a stack overflow. Untrusted input must be used to create a recursive datastructure, for example using JSON.parse, with no enforced depth limit. The datastructure thus created must be passed to _.flatten or _.isEqual. In the case of _.flatten, the vulnerability can only be exploited if it is possible for a remote client to prepare a datastructure that consists of arrays at all levels AND if no finite depth limit is passed as the second argument to _.flatten. In the case of _.isEqual, the vulnerability can only be exploited if there exists a code path in which two distinct datastructures that were submitted by the same remote client are compared using _.isEqual. For example, if a client submits data that are stored in a database, and the same client can later submit another datastructure that is then compared to the data that were saved in the database previously, OR if a client submits a single request, but its data are parsed twice, creating two non-identical but equivalent datastructures that are then compared. Exceptions originating from the call to _.flatten or _.isEqual, as a result of a stack overflow, are not being caught. This vulnerability is fixed in 1.13.8.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
underscorenpm | < 1.13.8 | 1.13.8 |
Affected products
1Patches
2a6e23ae96474Make _.isEqual nonrecursive
7 files changed · +481 −385
modules/isEqual.js+118 −94 modified@@ -12,127 +12,151 @@ import toBufferView from './_toBufferView.js'; // We use this string twice, so give it a name for minification. var tagDataView = '[object DataView]'; -// Internal recursive comparison function for `_.isEqual`. -function eq(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) return a !== 0 || 1 / a === 1 / b; - // `null` or `undefined` only equal to itself (strict comparison). - if (a == null || b == null) return false; - // `NaN`s are equivalent, but non-reflexive. - if (a !== a) return b !== b; - // Exhaust primitive checks - var type = typeof a; - if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; - return deepEq(a, b, aStack, bStack); -} +// Perform a deep comparison to check if two objects are equal. +export default function isEqual(a, b) { + // Keep track of which pairs of values need to be compared. We will be + // trampolining on this stack instead of using function recursion. + var todo = [{a: a, b: b}]; + // Initializing stacks of traversed objects for cycle detection. + var aStack = [], bStack = []; -// Internal recursive comparison function for `_.isEqual`. -function deepEq(a, b, aStack, bStack) { - // Unwrap any wrapped objects. - if (a instanceof _) a = a._wrapped; - if (b instanceof _) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className !== toString.call(b)) return false; - // Work around a bug in IE 10 - Edge 13. - if (hasDataViewBug && className == '[object Object]' && isDataView(a)) { - if (!isDataView(b)) return false; - className = tagDataView; - } - switch (className) { - // These types are compared by value. + // Keep traversing pairs until there is nothing left to compare. + while (todo.length) { + var frame = todo.pop(); + // As a special case, a single `true` on the todo means that we can + // unwind the cycle detection stacks. + if (frame === true) { + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + continue; + } + a = frame.a; + b = frame.b; + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + if (a !== 0 || 1 / a === 1 / b) continue; + return false; + } + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) { + if (b !== b) continue; + return false; + } + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasDataViewBug && className == '[object Object]' && isDataView(a)) { + if (!isDataView(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return '' + a === '' + b; + if ('' + a === '' + b) continue; + return false; case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. - // Object(NaN) is equivalent to NaN. - if (+a !== +a) return +b !== +b; - // An `egal` comparison is performed for other numeric values. - return +a === 0 ? 1 / +a === 1 / b : +a === +b; + todo.push({a: +a, b: +b}); + continue; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. - return +a === +b; + if (+a === +b) continue; + return false; case '[object Symbol]': - return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue; + return false; case '[object ArrayBuffer]': case tagDataView: // Coerce to typed array so we can fall through. - return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); - } + todo.push({a: toBufferView(a), b: toBufferView(b)}); + continue; + } - var areArrays = className === '[object Array]'; - if (!areArrays && isTypedArray(a)) { + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray(a)) { var byteLength = getByteLength(a); if (byteLength !== getByteLength(b)) return false; - if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue; areArrays = true; - } - if (!areArrays) { - if (typeof a != 'object' || typeof b != 'object') return false; - - // Objects with different constructors are not equivalent, but `Object`s or `Array`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && - isFunction(bCtor) && bCtor instanceof bCtor) - && ('constructor' in a && 'constructor' in b)) { - return false; } - } - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; - // Initializing stack of traversed objects. - // It's done here since we only need them for objects and arrays comparison. - aStack = aStack || []; - bStack = bStack || []; - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] === a) return bStack[length] === b; - } + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && + isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - // Recursively compare objects and arrays. - if (areArrays) { - // Compare array lengths to determine if a deep comparison is necessary. - length = a.length; - if (length !== b.length) return false; - // Deep compare the contents, ignoring non-numeric properties. + var length = aStack.length; while (length--) { - if (!eq(a[length], b[length], aStack, bStack)) return false; + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) { + // Cycle detected. Break out of the inner loop and continue the outer + // loop. Step 1: + if (bStack[length] === b) break; + return false; + } } - } else { - // Deep compare objects. - var _keys = keys(a), key; - length = _keys.length; - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b).length !== length) return false; - while (length--) { - // Deep compare each member - key = _keys[length]; - if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + // Step 2, use `length` to verify whether we detected a cycle: + if (length >= 0) continue; + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + // Remember to remove them again after the recursion below. + todo.push(true); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + todo.push({a: a[length], b: b[length]}); + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!has(b, key)) return false; + todo.push({a: a[key], b: b[key]}); + } } } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); + // We made it to the end and found no differences. return true; } - -// Perform a deep comparison to check if two objects are equal. -export default function isEqual(a, b) { - return eq(a, b); -}
underscore-esm.js+120 −96 modified@@ -348,131 +348,155 @@ function toBufferView(bufferSource) { // We use this string twice, so give it a name for minification. var tagDataView = '[object DataView]'; -// Internal recursive comparison function for `_.isEqual`. -function eq(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) return a !== 0 || 1 / a === 1 / b; - // `null` or `undefined` only equal to itself (strict comparison). - if (a == null || b == null) return false; - // `NaN`s are equivalent, but non-reflexive. - if (a !== a) return b !== b; - // Exhaust primitive checks - var type = typeof a; - if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; - return deepEq(a, b, aStack, bStack); -} - -// Internal recursive comparison function for `_.isEqual`. -function deepEq(a, b, aStack, bStack) { - // Unwrap any wrapped objects. - if (a instanceof _$1) a = a._wrapped; - if (b instanceof _$1) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className !== toString.call(b)) return false; - // Work around a bug in IE 10 - Edge 13. - if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { - if (!isDataView$1(b)) return false; - className = tagDataView; - } - switch (className) { - // These types are compared by value. +// Perform a deep comparison to check if two objects are equal. +function isEqual(a, b) { + // Keep track of which pairs of values need to be compared. We will be + // trampolining on this stack instead of using function recursion. + var todo = [{a: a, b: b}]; + // Initializing stacks of traversed objects for cycle detection. + var aStack = [], bStack = []; + + // Keep traversing pairs until there is nothing left to compare. + while (todo.length) { + var frame = todo.pop(); + // As a special case, a single `true` on the todo means that we can + // unwind the cycle detection stacks. + if (frame === true) { + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + continue; + } + a = frame.a; + b = frame.b; + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + if (a !== 0 || 1 / a === 1 / b) continue; + return false; + } + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) { + if (b !== b) continue; + return false; + } + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + + // Unwrap any wrapped objects. + if (a instanceof _$1) a = a._wrapped; + if (b instanceof _$1) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return '' + a === '' + b; + if ('' + a === '' + b) continue; + return false; case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. - // Object(NaN) is equivalent to NaN. - if (+a !== +a) return +b !== +b; - // An `egal` comparison is performed for other numeric values. - return +a === 0 ? 1 / +a === 1 / b : +a === +b; + todo.push({a: +a, b: +b}); + continue; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. - return +a === +b; + if (+a === +b) continue; + return false; case '[object Symbol]': - return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue; + return false; case '[object ArrayBuffer]': case tagDataView: // Coerce to typed array so we can fall through. - return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); - } + todo.push({a: toBufferView(a), b: toBufferView(b)}); + continue; + } - var areArrays = className === '[object Array]'; - if (!areArrays && isTypedArray$1(a)) { + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { var byteLength = getByteLength(a); if (byteLength !== getByteLength(b)) return false; - if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue; areArrays = true; - } - if (!areArrays) { - if (typeof a != 'object' || typeof b != 'object') return false; - - // Objects with different constructors are not equivalent, but `Object`s or `Array`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && - isFunction$1(bCtor) && bCtor instanceof bCtor) - && ('constructor' in a && 'constructor' in b)) { - return false; } - } - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - - // Initializing stack of traversed objects. - // It's done here since we only need them for objects and arrays comparison. - aStack = aStack || []; - bStack = bStack || []; - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] === a) return bStack[length] === b; - } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - // Recursively compare objects and arrays. - if (areArrays) { - // Compare array lengths to determine if a deep comparison is necessary. - length = a.length; - if (length !== b.length) return false; - // Deep compare the contents, ignoring non-numeric properties. + var length = aStack.length; while (length--) { - if (!eq(a[length], b[length], aStack, bStack)) return false; + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) { + // Cycle detected. Break out of the inner loop and continue the outer + // loop. Step 1: + if (bStack[length] === b) break; + return false; + } } - } else { - // Deep compare objects. - var _keys = keys(a), key; - length = _keys.length; - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b).length !== length) return false; - while (length--) { - // Deep compare each member - key = _keys[length]; - if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + // Step 2, use `length` to verify whether we detected a cycle: + if (length >= 0) continue; + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + // Remember to remove them again after the recursion below. + todo.push(true); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + todo.push({a: a[length], b: b[length]}); + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!has$1(b, key)) return false; + todo.push({a: a[key], b: b[key]}); + } } } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); + // We made it to the end and found no differences. return true; } -// Perform a deep comparison to check if two objects are equal. -function isEqual(a, b) { - return eq(a, b); -} - // Retrieve all the enumerable property names of an object. function allKeys(obj) { if (!isObject(obj)) return [];
underscore-esm.js.map+1 −1 modifiedunderscore-node-f.cjs+120 −96 modified@@ -350,131 +350,155 @@ function toBufferView(bufferSource) { // We use this string twice, so give it a name for minification. var tagDataView = '[object DataView]'; -// Internal recursive comparison function for `_.isEqual`. -function eq(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) return a !== 0 || 1 / a === 1 / b; - // `null` or `undefined` only equal to itself (strict comparison). - if (a == null || b == null) return false; - // `NaN`s are equivalent, but non-reflexive. - if (a !== a) return b !== b; - // Exhaust primitive checks - var type = typeof a; - if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; - return deepEq(a, b, aStack, bStack); -} - -// Internal recursive comparison function for `_.isEqual`. -function deepEq(a, b, aStack, bStack) { - // Unwrap any wrapped objects. - if (a instanceof _$1) a = a._wrapped; - if (b instanceof _$1) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className !== toString.call(b)) return false; - // Work around a bug in IE 10 - Edge 13. - if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { - if (!isDataView$1(b)) return false; - className = tagDataView; - } - switch (className) { - // These types are compared by value. +// Perform a deep comparison to check if two objects are equal. +function isEqual(a, b) { + // Keep track of which pairs of values need to be compared. We will be + // trampolining on this stack instead of using function recursion. + var todo = [{a: a, b: b}]; + // Initializing stacks of traversed objects for cycle detection. + var aStack = [], bStack = []; + + // Keep traversing pairs until there is nothing left to compare. + while (todo.length) { + var frame = todo.pop(); + // As a special case, a single `true` on the todo means that we can + // unwind the cycle detection stacks. + if (frame === true) { + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + continue; + } + a = frame.a; + b = frame.b; + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + if (a !== 0 || 1 / a === 1 / b) continue; + return false; + } + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) { + if (b !== b) continue; + return false; + } + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + + // Unwrap any wrapped objects. + if (a instanceof _$1) a = a._wrapped; + if (b instanceof _$1) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return '' + a === '' + b; + if ('' + a === '' + b) continue; + return false; case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. - // Object(NaN) is equivalent to NaN. - if (+a !== +a) return +b !== +b; - // An `egal` comparison is performed for other numeric values. - return +a === 0 ? 1 / +a === 1 / b : +a === +b; + todo.push({a: +a, b: +b}); + continue; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. - return +a === +b; + if (+a === +b) continue; + return false; case '[object Symbol]': - return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue; + return false; case '[object ArrayBuffer]': case tagDataView: // Coerce to typed array so we can fall through. - return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); - } + todo.push({a: toBufferView(a), b: toBufferView(b)}); + continue; + } - var areArrays = className === '[object Array]'; - if (!areArrays && isTypedArray$1(a)) { + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { var byteLength = getByteLength(a); if (byteLength !== getByteLength(b)) return false; - if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue; areArrays = true; - } - if (!areArrays) { - if (typeof a != 'object' || typeof b != 'object') return false; - - // Objects with different constructors are not equivalent, but `Object`s or `Array`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && - isFunction$1(bCtor) && bCtor instanceof bCtor) - && ('constructor' in a && 'constructor' in b)) { - return false; } - } - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - - // Initializing stack of traversed objects. - // It's done here since we only need them for objects and arrays comparison. - aStack = aStack || []; - bStack = bStack || []; - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] === a) return bStack[length] === b; - } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - // Recursively compare objects and arrays. - if (areArrays) { - // Compare array lengths to determine if a deep comparison is necessary. - length = a.length; - if (length !== b.length) return false; - // Deep compare the contents, ignoring non-numeric properties. + var length = aStack.length; while (length--) { - if (!eq(a[length], b[length], aStack, bStack)) return false; + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) { + // Cycle detected. Break out of the inner loop and continue the outer + // loop. Step 1: + if (bStack[length] === b) break; + return false; + } } - } else { - // Deep compare objects. - var _keys = keys(a), key; - length = _keys.length; - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b).length !== length) return false; - while (length--) { - // Deep compare each member - key = _keys[length]; - if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + // Step 2, use `length` to verify whether we detected a cycle: + if (length >= 0) continue; + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + // Remember to remove them again after the recursion below. + todo.push(true); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + todo.push({a: a[length], b: b[length]}); + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!has$1(b, key)) return false; + todo.push({a: a[key], b: b[key]}); + } } } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); + // We made it to the end and found no differences. return true; } -// Perform a deep comparison to check if two objects are equal. -function isEqual(a, b) { - return eq(a, b); -} - // Retrieve all the enumerable property names of an object. function allKeys(obj) { if (!isObject(obj)) return [];
underscore-node-f.cjs.map+1 −1 modifiedunderscore-umd.js+120 −96 modified@@ -357,131 +357,155 @@ // We use this string twice, so give it a name for minification. var tagDataView = '[object DataView]'; - // Internal recursive comparison function for `_.isEqual`. - function eq(a, b, aStack, bStack) { - // Identical objects are equal. `0 === -0`, but they aren't identical. - // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). - if (a === b) return a !== 0 || 1 / a === 1 / b; - // `null` or `undefined` only equal to itself (strict comparison). - if (a == null || b == null) return false; - // `NaN`s are equivalent, but non-reflexive. - if (a !== a) return b !== b; - // Exhaust primitive checks - var type = typeof a; - if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; - return deepEq(a, b, aStack, bStack); - } - - // Internal recursive comparison function for `_.isEqual`. - function deepEq(a, b, aStack, bStack) { - // Unwrap any wrapped objects. - if (a instanceof _$1) a = a._wrapped; - if (b instanceof _$1) b = b._wrapped; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className !== toString.call(b)) return false; - // Work around a bug in IE 10 - Edge 13. - if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { - if (!isDataView$1(b)) return false; - className = tagDataView; - } - switch (className) { - // These types are compared by value. + // Perform a deep comparison to check if two objects are equal. + function isEqual(a, b) { + // Keep track of which pairs of values need to be compared. We will be + // trampolining on this stack instead of using function recursion. + var todo = [{a: a, b: b}]; + // Initializing stacks of traversed objects for cycle detection. + var aStack = [], bStack = []; + + // Keep traversing pairs until there is nothing left to compare. + while (todo.length) { + var frame = todo.pop(); + // As a special case, a single `true` on the todo means that we can + // unwind the cycle detection stacks. + if (frame === true) { + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + continue; + } + a = frame.a; + b = frame.b; + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](https://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { + if (a !== 0 || 1 / a === 1 / b) continue; + return false; + } + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) { + if (b !== b) continue; + return false; + } + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + + // Unwrap any wrapped objects. + if (a instanceof _$1) a = a._wrapped; + if (b instanceof _$1) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + // Work around a bug in IE 10 - Edge 13. + if (hasDataViewBug && className == '[object Object]' && isDataView$1(a)) { + if (!isDataView$1(b)) return false; + className = tagDataView; + } + switch (className) { + // These types are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. - return '' + a === '' + b; + if ('' + a === '' + b) continue; + return false; case '[object Number]': - // `NaN`s are equivalent, but non-reflexive. - // Object(NaN) is equivalent to NaN. - if (+a !== +a) return +b !== +b; - // An `egal` comparison is performed for other numeric values. - return +a === 0 ? 1 / +a === 1 / b : +a === +b; + todo.push({a: +a, b: +b}); + continue; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. - return +a === +b; + if (+a === +b) continue; + return false; case '[object Symbol]': - return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + if (SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b)) continue; + return false; case '[object ArrayBuffer]': case tagDataView: // Coerce to typed array so we can fall through. - return deepEq(toBufferView(a), toBufferView(b), aStack, bStack); - } + todo.push({a: toBufferView(a), b: toBufferView(b)}); + continue; + } - var areArrays = className === '[object Array]'; - if (!areArrays && isTypedArray$1(a)) { + var areArrays = className === '[object Array]'; + if (!areArrays && isTypedArray$1(a)) { var byteLength = getByteLength(a); if (byteLength !== getByteLength(b)) return false; - if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true; + if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) continue; areArrays = true; - } - if (!areArrays) { - if (typeof a != 'object' || typeof b != 'object') return false; - - // Objects with different constructors are not equivalent, but `Object`s or `Array`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && - isFunction$1(bCtor) && bCtor instanceof bCtor) - && ('constructor' in a && 'constructor' in b)) { - return false; } - } - // Assume equality for cyclic structures. The algorithm for detecting cyclic - // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - - // Initializing stack of traversed objects. - // It's done here since we only need them for objects and arrays comparison. - aStack = aStack || []; - bStack = bStack || []; - var length = aStack.length; - while (length--) { - // Linear search. Performance is inversely proportional to the number of - // unique nested structures. - if (aStack[length] === a) return bStack[length] === b; - } + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction$1(aCtor) && aCtor instanceof aCtor && + isFunction$1(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } - // Add the first object to the stack of traversed objects. - aStack.push(a); - bStack.push(b); + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. - // Recursively compare objects and arrays. - if (areArrays) { - // Compare array lengths to determine if a deep comparison is necessary. - length = a.length; - if (length !== b.length) return false; - // Deep compare the contents, ignoring non-numeric properties. + var length = aStack.length; while (length--) { - if (!eq(a[length], b[length], aStack, bStack)) return false; + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) { + // Cycle detected. Break out of the inner loop and continue the outer + // loop. Step 1: + if (bStack[length] === b) break; + return false; + } } - } else { - // Deep compare objects. - var _keys = keys(a), key; - length = _keys.length; - // Ensure that both objects contain the same number of properties before comparing deep equality. - if (keys(b).length !== length) return false; - while (length--) { - // Deep compare each member - key = _keys[length]; - if (!(has$1(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + // Step 2, use `length` to verify whether we detected a cycle: + if (length >= 0) continue; + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + // Remember to remove them again after the recursion below. + todo.push(true); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + todo.push({a: a[length], b: b[length]}); + } + } else { + // Deep compare objects. + var _keys = keys(a), key; + length = _keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = _keys[length]; + if (!has$1(b, key)) return false; + todo.push({a: a[key], b: b[key]}); + } } } - // Remove the first object from the stack of traversed objects. - aStack.pop(); - bStack.pop(); + // We made it to the end and found no differences. return true; } - // Perform a deep comparison to check if two objects are equal. - function isEqual(a, b) { - return eq(a, b); - } - // Retrieve all the enumerable property names of an object. function allKeys(obj) { if (!isObject(obj)) return [];
underscore-umd.js.map+1 −1 modified
411e222eb0caMake internal flatten nonrecursive
7 files changed · +87 −79
modules/_flatten.js+21 −19 modified@@ -3,26 +3,28 @@ import isArrayLike from './_isArrayLike.js'; import isArray from './isArray.js'; import isArguments from './isArguments.js'; -// Internal implementation of a recursive `flatten` function. -export default function flatten(input, depth, strict, output) { - output = output || []; - if (!depth && depth !== 0) { - depth = Infinity; - } else if (depth <= 0) { - return output.concat(input); - } - var idx = output.length; - for (var i = 0, length = getLength(input); i < length; i++) { - var value = input[i]; - if (isArrayLike(value) && (isArray(value) || isArguments(value))) { +// Internal implementation of a `flatten` function. +export default function flatten(input, depth, strict) { + if (!depth && depth !== 0) depth = Infinity; + var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = []; + while (true) { + if (i >= length) { + if (!stack.length) break; + var frame = stack.pop(); + i = frame.i; + input = frame.v; + length = getLength(input); + continue; + } + var value = input[i++]; + if (stack.length >= depth) { + output[idx++] = value; + } else if (isArrayLike(value) && (isArray(value) || isArguments(value))) { // Flatten current level of array or arguments object. - if (depth > 1) { - flatten(value, depth - 1, strict, output); - idx = output.length; - } else { - var j = 0, len = value.length; - while (j < len) output[idx++] = value[j++]; - } + stack.push({i: i, v: input}); + i = 0; + input = value; + length = getLength(input); } else if (!strict) { output[idx++] = value; }
underscore-esm.js+21 −19 modified@@ -1022,26 +1022,28 @@ var bind = restArguments(function(func, context, args) { // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var isArrayLike = createSizePropertyCheck(getLength); -// Internal implementation of a recursive `flatten` function. -function flatten$1(input, depth, strict, output) { - output = output || []; - if (!depth && depth !== 0) { - depth = Infinity; - } else if (depth <= 0) { - return output.concat(input); - } - var idx = output.length; - for (var i = 0, length = getLength(input); i < length; i++) { - var value = input[i]; - if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { +// Internal implementation of a `flatten` function. +function flatten$1(input, depth, strict) { + if (!depth && depth !== 0) depth = Infinity; + var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = []; + while (true) { + if (i >= length) { + if (!stack.length) break; + var frame = stack.pop(); + i = frame.i; + input = frame.v; + length = getLength(input); + continue; + } + var value = input[i++]; + if (stack.length >= depth) { + output[idx++] = value; + } else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { // Flatten current level of array or arguments object. - if (depth > 1) { - flatten$1(value, depth - 1, strict, output); - idx = output.length; - } else { - var j = 0, len = value.length; - while (j < len) output[idx++] = value[j++]; - } + stack.push({i: i, v: input}); + i = 0; + input = value; + length = getLength(input); } else if (!strict) { output[idx++] = value; }
underscore-esm.js.map+1 −1 modifiedunderscore-node-f.cjs+21 −19 modified@@ -1024,26 +1024,28 @@ var bind = restArguments(function(func, context, args) { // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var isArrayLike = createSizePropertyCheck(getLength); -// Internal implementation of a recursive `flatten` function. -function flatten$1(input, depth, strict, output) { - output = output || []; - if (!depth && depth !== 0) { - depth = Infinity; - } else if (depth <= 0) { - return output.concat(input); - } - var idx = output.length; - for (var i = 0, length = getLength(input); i < length; i++) { - var value = input[i]; - if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { +// Internal implementation of a `flatten` function. +function flatten$1(input, depth, strict) { + if (!depth && depth !== 0) depth = Infinity; + var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = []; + while (true) { + if (i >= length) { + if (!stack.length) break; + var frame = stack.pop(); + i = frame.i; + input = frame.v; + length = getLength(input); + continue; + } + var value = input[i++]; + if (stack.length >= depth) { + output[idx++] = value; + } else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { // Flatten current level of array or arguments object. - if (depth > 1) { - flatten$1(value, depth - 1, strict, output); - idx = output.length; - } else { - var j = 0, len = value.length; - while (j < len) output[idx++] = value[j++]; - } + stack.push({i: i, v: input}); + i = 0; + input = value; + length = getLength(input); } else if (!strict) { output[idx++] = value; }
underscore-node-f.cjs.map+1 −1 modifiedunderscore-umd.js+21 −19 modified@@ -1031,26 +1031,28 @@ // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var isArrayLike = createSizePropertyCheck(getLength); - // Internal implementation of a recursive `flatten` function. - function flatten$1(input, depth, strict, output) { - output = output || []; - if (!depth && depth !== 0) { - depth = Infinity; - } else if (depth <= 0) { - return output.concat(input); - } - var idx = output.length; - for (var i = 0, length = getLength(input); i < length; i++) { - var value = input[i]; - if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { + // Internal implementation of a `flatten` function. + function flatten$1(input, depth, strict) { + if (!depth && depth !== 0) depth = Infinity; + var output = [], idx = 0, i = 0, length = getLength(input) || 0, stack = []; + while (true) { + if (i >= length) { + if (!stack.length) break; + var frame = stack.pop(); + i = frame.i; + input = frame.v; + length = getLength(input); + continue; + } + var value = input[i++]; + if (stack.length >= depth) { + output[idx++] = value; + } else if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) { // Flatten current level of array or arguments object. - if (depth > 1) { - flatten$1(value, depth - 1, strict, output); - idx = output.length; - } else { - var j = 0, len = value.length; - while (j < len) output[idx++] = value[j++]; - } + stack.push({i: i, v: input}); + i = 0; + input = value; + length = getLength(input); } else if (!strict) { output[idx++] = value; }
underscore-umd.js.map+1 −1 modified
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
9- github.com/jashkenas/underscore/commit/411e222eb0ca5d570cc4f6315c02c05b830ed2b4nvdPatchWEB
- github.com/jashkenas/underscore/commit/a6e23ae9647461ec33ad9f92a2ecfc220eea0a84nvdPatchWEB
- github.com/jashkenas/underscore/security/advisories/GHSA-qpx9-hpmf-5gmwnvdExploitPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-qpx9-hpmf-5gmwghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27601ghsaADVISORY
- github.com/jashkenas/underscore/issues/3011ghsaWEB
- underscorejs.orgghsaWEB
- underscorejs.orgghsaWEB
- underscorejs.orgghsaWEB
News mentions
9- State of ransomware in 2026Securelist · May 12, 2026
- The State of Ransomware – Q1 2026Check Point Research · May 11, 2026
- CVE-2025-68670: discovering an RCE vulnerability in xrdpSecurelist · May 8, 2026
- MuddyWater Uses Microsoft Teams to Steal Credentials in False Flag Ransomware AttackThe Hacker News · May 6, 2026
- China-Linked UAT-8302 Targets Governments Using Shared APT Malware Across RegionsThe Hacker News · May 5, 2026
- Shadow IT has given way to shadow AI. Enter AI-BOMsThe Register Security · May 4, 2026
- Shadow IT has given way to shadow AI. Enter AI-BOMsThe Register Security · May 4, 2026
- Shutdowns, power outages, and conflict: a review of Q1 2026 Internet disruptionsCloudflare Blog · Apr 28, 2026
- FIRESTARTER Backdoor Hit Federal Cisco Firepower Device, Survives Security PatchesThe Hacker News · Apr 24, 2026