Skip to content

Commit fe8d32e

Browse files
UlisesGasconfalsyvalues
authored andcommitted
fix: block prototype pollution in baseUnset via constructor/prototype traversal
Block `constructor` and `prototype` unconditionally as non-terminal traversal keys in baseUnset, matching the approach already used by baseSet. The previous guard only blocked the specific two-key sequence `constructor` → `prototype`, allowing attackers to: - Delete static methods from built-in constructors (Object.keys, Array.isArray, String.fromCharCode) via paths like `['constructor', 'keys']` - Delete built-in prototype methods (toFixed, toLowerCase, valueOf) via primitive roots like `_.unset(0, 'constructor.prototype.toFixed')` - Bypass checks entirely using array-wrapped path segments like `[['constructor'], ['keys']]` which evaded the string-only key check The primitive root exception that previously allowed constructor.prototype traversal from primitives (e.g., `_.unset(0, 'constructor.prototype.a')`) is removed as it enabled deletion of properties on shared built-in prototypes. Path segments are now normalized with toKey() before validation.
1 parent 18ba0a3 commit fe8d32e

5 files changed

Lines changed: 175 additions & 104 deletions

File tree

dist/lodash.js

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4375,40 +4375,28 @@
43754375
function baseUnset(object, path) {
43764376
path = castPath(path, object);
43774377

4378-
// Prevent prototype pollution, see: https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg
4378+
// Prevent prototype pollution:
4379+
// https://github.com/lodash/lodash/security/advisories/GHSA-xxjr-mmjv-4gpg
4380+
// https://github.com/lodash/lodash/security/advisories/GHSA-f23m-r3pf-42rh
4381+
// https://github.com/lodash/lodash/security/advisories/GHSA-w36w-cm3g-pc62
43794382
var index = -1,
43804383
length = path.length;
43814384

43824385
if (!length) {
43834386
return true;
43844387
}
43854388

4386-
var isRootPrimitive = object == null || (typeof object !== 'object' && typeof object !== 'function');
4387-
43884389
while (++index < length) {
4389-
var key = path[index];
4390-
4391-
// skip non-string keys (e.g., Symbols, numbers)
4392-
if (typeof key !== 'string') {
4393-
continue;
4394-
}
4390+
var key = toKey(path[index]);
43954391

43964392
// Always block "__proto__" anywhere in the path if it's not expected
43974393
if (key === '__proto__' && !hasOwnProperty.call(object, '__proto__')) {
43984394
return false;
43994395
}
44004396

4401-
// Block "constructor.prototype" chains
4402-
if (key === 'constructor' &&
4403-
(index + 1) < length &&
4404-
typeof path[index + 1] === 'string' &&
4405-
path[index + 1] === 'prototype') {
4406-
4407-
// Allow ONLY when the path starts at a primitive root, e.g., _.unset(0, 'constructor.prototype.a')
4408-
if (isRootPrimitive && index === 0) {
4409-
continue;
4410-
}
4411-
4397+
// Block constructor/prototype as non-terminal traversal keys to prevent
4398+
// escaping the object graph into built-in constructors and prototypes.
4399+
if ((key === 'constructor' || key === 'prototype') && index < length - 1) {
44124400
return false;
44134401
}
44144402
}

0 commit comments

Comments
 (0)