How Lodash omit Parses Nested String Paths
This article examines the internal path resolution pipeline used by
Lodash's _.omit function to securely parse and traverse
heavily nested string descriptors. It details how complex path
notations—such as dot delimiters and bracket accessors—are transformed
into discrete property tokens through internal utilities like
castPath and stringToPath, and outlines the
security boundaries that mitigate prototype pollution and execution
errors during deep object traversal.
The Path Normalization
Pipeline: castPath
Before any object manipulation occurs, _.omit normalizes
incoming property identifiers. While paths can be supplied as native
arrays of keys (e.g., ['a', '0', 'b']), heavily nested
descriptors are commonly passed as strings (e.g.,
'a[0].b.c').
Lodash processes these identifiers through an internal utility named
castPath:
function castPath(value, object) {
if (Array.isArray(value)) {
return value;
}
return isKey(value, object) ? [value] : stringToPath(value);
}isKeyEvaluation: Lodash checks if the input is a direct, un-nested property name on the target object or if it avoids deep-path syntax (such as dots, open brackets, or property separators).- Delegation to
stringToPath: If the string contains deep accessor syntax, it is handed off tostringToPathto be broken down into normalized key segments.
Tokenizing
Nested String Descriptors: stringToPath
The core parsing engine for string descriptors resides in
stringToPath. This function converts path strings into
uniform string keys using a regular expression designed to handle dots,
brackets, integers, and quoted keys.
The parser uses the following regular expression logic:
const rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;This pattern matches three specific structures:
- Standard Key Names (
[^.[\]]+): Matches contiguous alphanumeric characters and symbols excluding.,[, and]. - Bracket-Enclosed Expressions
(
\[...\]): Captures array indices, floating-point numbers, or string keys wrapped in matching single or double quotes while accounting for escaped characters. - Empty Bracket or Dot Transitions: Accurately tracks consecutive separators without dropping context.
To optimize performance across repetitive parsing cycles, Lodash
caches the results using memoizeCapped, ensuring that
recurring, deeply nested descriptors are compiled only once.
Security Constraints and Prototype Pollution Defense
Parsing arbitrary paths from user input introduces the risk of
Prototype Pollution attacks, where keys like __proto__,
constructor, or prototype manipulate base
object behaviors. Lodash enforces strict defense mechanisms across its
traversal operations:
1. Key Canonicalization via
toKey
Each parsed token is passed through toKey, ensuring
symbol handling and converting numbers to strings without evaluating
expressions that could execute arbitrary code.
2. Disarming Sensitive Object References
When deep paths are traversed to perform omissions, internal
traversal routines verify that keys referring to __proto__,
prototype, or constructor do not allow
mutations on the global prototype chain. If a traversal encounters an
accessor targeting Object.prototype, the path resolution
either isolates mutations to a cloned instance or drops invalid
assignments.
3. Safe Traversal (No Nullish Dereference Errors)
Traditional JavaScript path resolution throws a
TypeError when evaluating an accessor on an
undefined or null intermediate property (e.g.,
evaluating x.y.z where x.y is
null). Lodash resolves this by verifying existence at each
step using internal checks (hasIn or shallow presence
checks). If an intermediate parent in the path does not exist or is not
an object, the resolver aborts the path traversal safely without
throwing exceptions.
Path Deletion via
baseClone and baseUnset
Unlike mutative operations (such as _.unset),
_.omit is non-destructive. It does not delete properties
directly from the input reference:
- Shallow Copy Creation: Lodash clones the base object.
- Path Unsetting: It calls
baseUnset, which traverses the cloned structure down to the target parent node:function baseUnset(object, path) { path = castPath(path, object); object = parent(object, path); return object == null || delete object[toKey(last(path))]; } - Deep Isolation: The terminal token retrieved from
last(path)is deleted from the intermediate parent object. If intermediate containers are primitives or inaccessible, the operation exits silently and leaves valid siblings intact.