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);
}
  1. isKey Evaluation: 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).
  2. Delegation to stringToPath: If the string contains deep accessor syntax, it is handed off to stringToPath to 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:

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:

  1. Shallow Copy Creation: Lodash clones the base object.
  2. 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))];
    }
  3. 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.