
# 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.
::

```ts
import { safeJsonParse } from "unsecure/sanitize";

safeJsonParse('{"a":1,"__proto__":{"admin":true},"n":{"constructor":2}}');
// { a: 1, n: {} }
```

## Signatures

```ts
// 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`           |

```ts
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. `sanitizeObject` tracks visited nodes in a `WeakSet`; `sanitizeObjectCopy` uses a `WeakMap` and rewires cycles to point at the copied node.
- Only own properties named exactly `__proto__`, `prototype` and `constructor` are removed. `sanitizeObject` enumerates own property **names**, so a non-enumerable `__proto__` planted with `Object.defineProperty` is removed too.
- No getter is ever invoked. Every value comes from its property descriptor, array elements included. `sanitizeObject` leaves accessors in place and deletes a dangerous name unread; `sanitizeObjectCopy` copies 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.

```ts
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` |

```ts
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

```ts
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

```ts
// 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:

```ts
const plain = safeJsonParse(JSON.stringify(obj));
```
