
# Parse untrusted JSON

> One call that parses and removes the three key names that turn data into instructions.

## The problem

`__proto__` is not an ordinary property name in JavaScript. Assigning to it changes an object's prototype, and a prototype is shared. So a request body like this is not data:

```json
{ "name": "alice", "__proto__": { "isAdmin": true } }
```

`JSON.parse` itself is safe — it creates a plain own property — but almost anything you do next is not. A deep merge, a clone, a config loader, an ORM's hydrate step: any of them can carry the assignment out, and every object in the process inherits `isAdmin: true`.

## The fix

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

const payload = safeJsonParse('{"a":1,"__proto__":{"admin":true},"n":{"constructor":2}}');
// { a: 1, n: {} }
Object.keys(payload); // ["a", "n"]
```

`safeJsonParse` parses, then walks the result and deletes every own property named exactly `__proto__`, `prototype` or `constructor`, at any depth. Any JSON root works: an object, an array, or a primitive.

## In a handler

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

export async function readBody<T>(request: Request): Promise<T | null> {
  const text = await request.text();
  try {
    return safeJsonParse<T>(text);
  } catch (error) {
    if (error instanceof UnsecureError && error.code === "MALFORMED") return null;
    throw error;
  }
}
```

Unparseable JSON is `MALFORMED`, with the engine's own `SyntaxError` in `cause` — that is the half that says where the text went wrong, so log it.

```ts
safeJsonParse("{oops");
// UnsecureError MALFORMED: safeJsonParse: Expected property name or '}' in JSON at position 1 (line 1 column 2)
```

## When the framework already parsed

Most frameworks hand you a parsed body. Sanitize that instead.

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

// Express, Hono, Fastify: the body is yours, so mutate it
function sanitizeMiddleware(req, res, next) {
  if (req.body && typeof req.body === "object") sanitizeObject(req.body);
  next();
}
```

`sanitizeObject` returns the same reference it was given and is the cheapest of the three: a single pass per node, no intermediate allocations.

If the object is not yours to change, copy it:

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

const safe = sanitizeObjectCopy(caller.body); // caller.body is untouched
```

## What is and is not covered

Covered: own properties named `__proto__`, `prototype` or `constructor`, on arrays and objects, at any depth, including non-enumerable ones planted with `Object.defineProperty`, and including cycles.

Not covered: the contents of a `Map`, a `Set`, a class instance or any other non-plain object. `sanitizeObjectCopy` carries those into the copy by reference, which means unchanged and also unsanitized.

```ts
// A strictly plain, fully walked structure
const plain = safeJsonParse(JSON.stringify(obj));
```

Nothing here reads a getter. Every value comes from its property descriptor, array elements included, so traversing an object never runs code the object carries. A `Proxy` is the one exception and cannot be otherwise: its traps run for every property operation, `getOwnPropertyDescriptor` included. Sanitize the target rather than the proxy when that matters.

## Frozen input

```ts
sanitizeObject(Object.freeze(JSON.parse('{"__proto__":{"x":1}}')));
// UnsecureError FROZEN: sanitizeObject: cannot remove "__proto__" from a frozen object; use sanitizeObjectCopy().
```

A frozen object cannot have the key removed, and reporting success would be a lie. Use the copy variant.

## Depth

All three functions walk an explicit stack, so nesting depth is bounded by memory rather than by the call stack. A payload a few kilobytes long, nested ten thousand levels deep, is handled — the kind of input that used to be a denial-of-service against a recursive sanitizer.

That is not a substitute for a body size limit. Set one.
