Please report security issues privately via GitHub Security Advisories ("Report a vulnerability" on the repo's Security tab) rather than a public issue.
neotraverse walks, clones, merges, diffs, and patches arbitrary JavaScript objects. It is commonly used on untrusted input — objects from JSON.parse, request bodies, config files, JSON Schema / OpenAPI documents — and with untrusted path / JSON-Pointer / glob strings. The hardening below targets that model. Findings are gated in CI by test/security.test.ts, test/functional-security.test.ts, and test/security-audit.test.ts (see .github/workflows/security.yml).
- Prototype pollution.
set/setPath/merge/clone/patch/dereferencereject or neutralize__proto__/constructor/prototype. Keys are coerced to a primitive once before the guard, so a boxed/object key (new String('__proto__'),{toString:…}) or a TOCTOUtoStringcannot slip past the check or fire the__proto__setter. Injected__proto__from JSON is kept as an inert own data property, never the prototype. sanitizefor the trust boundary. If you must hand untrusted parsed JSON to code that is not pollution-hardened (a naive deep-merge, an ORM, a template engine),sanitize(obj)returns a deep clone with every own__proto__/constructor/prototypekey removed at every level. It strips the key-injection vector only — it does not bound depth/size or sanitize path-based writes, and it is not a blanket "make this safe" guarantee.- DoS bounding.
merge,diff, anddeepEqualare cycle-safe (they terminate on circular input);dereferencefollows$refchains iteratively (no stack overflow) and memoizes resolved targets (no O(N²) re-walk);diffmemoizes equal pairs (no exponential blow-up on shared/DAG inputs);deepEqual/diffcompare primitive Sets in O(n). - Type confusion. Cross-realm
Map/Set/DataView/ArrayBufferandSymbol.toStringTagspoofs are detected by tag and degrade gracefully — they never crashclone, lose entries, or return the input by reference. - Isolation.
cloneandmergereturn values that share no references with their inputs (including undermap()+stop(), andError.cause).
These are intentional behaviors, not bugs — handle them at the call site:
- Recursion depth is unbounded by default. Deeply-nested untrusted input can overflow the call stack. Pass
maxDepthto bound it:clone(x, { maxDepth: 1000 }),deepEqual(a, b, { maxDepth }),diff(a, b, { maxDepth }),merge(a, b, { maxDepth }),dereference(doc, { maxDepth }), walks (forEach/map/…). dereferencefan-out is proportional to the document. N$refs to a large subtree produce N clones — size-limit untrusted schema/OpenAPI documents before dereferencing.deepEqual/diffon large object-element Sets is O(n²). Size-cap untrusted Sets of objects (primitive-element Sets are O(n)).patchrejects unsafe path segments (__proto__/constructor/prototype) by design. As a consequence, objects with a literal own__proto__data key do not round-trip throughdiff→patch(the patch throws rather than risk pollution). This is intentional.- Traversal executes accessors.
forEach/map/clone/get/has/deepEqualreadobj[key], which invokes getters andProxytraps on the input. If the input is untrusted and may carry accessors, round-trip it throughJSON.parse(JSON.stringify(x))(orstructuredClone) first. The library is robust to throwing/mutating getters (no state corruption, no infinite loop), but their code still runs. get/hasread own non-enumerable properties (they never walk the prototype chain), which differs from the walk APIs' own-enumerable-only contract.freezedeep-freezes the original graph in place (and all shared sub-objects). Usefreeze(clone(obj))if you need an isolated frozen copy.
Security fixes target the latest published 0.x release.