Sanitize
Strip __proto__, prototype and constructor out of untrusted data, at parse time, in place, or into a copy.
Note
ELI5. In JavaScript, an object named __proto__ is not an ordinary key: assigning to it can change the behaviour of every object in the program. So a request body containing {"__proto__": {"isAdmin": true}} is not data, it is an instruction, and a careless merge or clone can carry it out. These three functions walk a value and delete those three names wherever they appear as own properties. Use safeJsonParse on text that came from outside. Use the others when you already have the parsed object.
import { safeJsonParse } from "unsecure/sanitize";
safeJsonParse('{"a":1,"__proto__":{"admin":true},"n":{"constructor":2}}');
// { a: 1, n: {} }#Signatures
// Mutates the input and returns the same reference
function sanitizeObject<T extends Record<string, unknown> | undefined>(obj: T): T;
// Returns a sanitized deep copy; the input is never touched. Cycle-safe.
function sanitizeObjectCopy<T extends Record<string, unknown> | undefined>(obj: T): T;
// JSON.parse, then the in-place sanitizer. Any JSON root.
function safeJsonParse<T = any>(json: string): T;#Which one to use
| If you… | Use |
|---|---|
| Have JSON text and want one safe step | safeJsonParse |
| Already parsed, and the object is yours to mutate | sanitizeObject — the fastest |
| Must leave the caller's object exactly as it was | sanitizeObjectCopy |
import { safeJsonParse, sanitizeObject, sanitizeObjectCopy } from "unsecure/sanitize";
// 1. Parse and sanitize together
const payload = safeJsonParse<{ user: { name: string } }>(untrustedText);
// 2. Post-parse, in place — cheapest on a hot path
const parsed = JSON.parse(untrustedText);
sanitizeObject(parsed); // the same reference comes back
// 3. Post-parse, without touching what you were given
const safe = sanitizeObjectCopy(caller.body);#What they do
- Deep traversal over objects and arrays, driven by an explicit stack. Nesting depth is bounded by memory, not by the call stack, so a deeply nested payload cannot overflow it.
- Cycle-safe.
sanitizeObjecttracks visited nodes in aWeakSet;sanitizeObjectCopyuses aWeakMapand rewires cycles to point at the copied node. - Only own properties named exactly
__proto__,prototypeandconstructorare removed.sanitizeObjectenumerates own property names, so a non-enumerable__proto__planted withObject.definePropertyis removed too. - No getter is ever invoked. Every value comes from its property descriptor, array elements included.
sanitizeObjectleaves accessors in place and deletes a dangerous name unread;sanitizeObjectCopycopies own enumerable data properties only, so accessors do not appear in the copy and an accessor at an array index leaves a hole rather than shifting later elements. - Object identity survives
sanitizeObject: it strips keys and never replaces an object.
const obj = JSON.parse('{"a":1,"__proto__":{"x":1}}');
sanitizeObject(obj) === obj; // true
obj; // { a: 1 }
const input = JSON.parse('{"a":1,"__proto__":{"x":1}}');
sanitizeObjectCopy(input); // { a: 1 }
Object.keys(input); // ["a", "__proto__"] — untouched#What they do not do
sanitizeObjectCopy descends only into arrays and plain objects, meaning objects rooted on Object.prototype or on null. Every other value — a Date, Map, Set, typed array, RegExp, class instance, function — is carried into the copy by reference. So copy.when === input.when for a Date.
Carried by reference means unchanged and also unsanitized. Neither function reads what a Map holds, what a Set contains, or what a class instance keeps in its own state. If untrusted material lives inside one of those, run it through safeJsonParse(JSON.stringify(x)) or sanitize it yourself.
A Proxy is the exception and cannot be otherwise: its traps run for every property operation, getOwnPropertyDescriptor included, so a proxied object is traversed through its own traps and proxied code does run. Sanitize the target rather than the proxy when that matters.
sanitizeObjectCopy returns plain objects rooted on Object.prototype even when the input had a null prototype. undefined and non-object inputs come back unchanged, as does a copy root that is neither an array nor a plain object.
#Errors
| Cause | code |
|---|---|
| A dangerous key cannot be deleted because the object is frozen or sealed | FROZEN |
safeJsonParse was handed text that is not JSON | MALFORMED |
sanitizeObject(Object.freeze(JSON.parse('{"__proto__":{"x":1}}')));
// UnsecureError FROZEN: sanitizeObject: cannot remove "__proto__" from a frozen object; use sanitizeObjectCopy().
safeJsonParse("{oops");
// UnsecureError MALFORMED: safeJsonParse: Expected property name or '}' in JSON at position 1 (line 1 column 2)The MALFORMED error keeps the engine's own SyntaxError in cause, which is the half that says where the text went wrong.
#Recipe: sanitize a request body
import { safeJsonParse, sanitizeObject } from "unsecure/sanitize";
// A framework that already parsed for you: mutate in place
function sanitizeMiddleware(req, res, next) {
if (req.body && typeof req.body === "object") sanitizeObject(req.body);
next();
}
// Reading the body yourself: parse and sanitize in one step
async function readBody(request: Request) {
return safeJsonParse(await request.text());
}#Performance
sanitizeObject makes a single pass over each node. It iterates Object.getOwnPropertyNames once with the dangerous-key check inlined into that loop, pays one getOwnPropertyDescriptor per surviving key — the price of never calling a getter — and pushes children onto a traversal stack without allocating an intermediate array. Arrays iterate with a numeric loop, which beats Object.keys on dense arrays.
For deep trees that noticeably reduces both allocations and branches compared to a scan-then-walk approach.
#Pitfall: mutating an object you do not own
// The caller still holds this reference and did not ask for it to change
const original = JSON.parse(data);
sanitizeObject(original);
// Copy instead
const safe = sanitizeObjectCopy(caller.body);#Pitfall: assuming the sanitizers harden non-plain objects
They strip dangerous own properties from arrays and plain objects. They do not re-home class instances, convert a Map, or alter a prototype chain. A Date reached from a sanitized tree is the same object it was. For a strictly plain, fully-walked structure:
const plain = safeJsonParse(JSON.stringify(obj));