JSON Patch is a standardized format (RFC 6902) for describing partial changes to a JSON document, and its main security concern is that naively applying a patch from an untrusted source can enable prototype pollution, which is exactly the flaw that affected the popular fast-json-patch library before version 3.1.1 (CVE-2021-4279). If you accept JSON Patch operations from clients, the path values in those operations are attacker-controlled and must be treated as such. This guide explains the format, the risk, and safe usage.
The appeal of JSON Patch is bandwidth and clarity. Instead of sending an entire updated object, a client sends a small array of operations describing what changed. It is widely used in REST APIs (often via the application/json-patch+json content type), in collaborative editing, and in configuration systems.
How JSON Patch works
A patch is an array of operation objects. Each has an op, a path written in JSON Pointer syntax, and depending on the operation, a value or from:
[
{ "op": "replace", "path": "/email", "value": "new@example.com" },
{ "op": "add", "path": "/roles/-", "value": "editor" },
{ "op": "remove", "path": "/tempFlag" }
]
The defined operations are add, remove, replace, move, copy, and test. The test op is a guard: it asserts a value equals something and aborts the whole patch if not, which is useful for optimistic concurrency. The path uses JSON Pointer, where /roles/0 addresses the first element of a roles array and /- appends.
That path is the crux of the security story. It is a string that tells the library where in your object to write, and if it comes from a client, the client chooses where you write.
The prototype pollution risk
Prototype pollution is a JavaScript-specific vulnerability where an attacker sets properties on Object.prototype, which then appear on every object in the program. In the JSON Patch context, the attack is a path that navigates into the prototype chain:
[
{ "op": "add", "path": "/__proto__/isAdmin", "value": true }
]
If the patch library resolves that path by walking properties without guarding against __proto__, constructor, and prototype, applying it writes isAdmin: true onto Object.prototype. Suddenly every object in your process reports isAdmin === true unless it explicitly overrides the field. Depending on how your code makes decisions, that can escalate to authorization bypass, denial of service, or in some setups remote code execution.
This is not hypothetical. fast-json-patch, one of the most-used JSON Patch implementations on npm, was vulnerable to prototype pollution through crafted paths. It was tracked as CVE-2021-4279 and fixed in version 3.1.1, which added guards against the dangerous keys. Confirm the detail against the Snyk advisory for CVE-2021-4279 rather than any single summary.
Using fast-json-patch safely
If you use the fast json patch library, the baseline is to be on a patched version:
npm ls fast-json-patch
npm install fast-json-patch@latest # 3.1.1 or newer
But version alone is not the whole defense when you apply patches from untrusted clients. Layer these controls:
Validate the shape before applying. Reject any operation whose path contains __proto__, constructor, or prototype outright. A cheap allowlist or denylist on the path string stops the classic attack before it reaches the patch engine.
Restrict which paths are writable. Most APIs only intend to expose a handful of fields for patching. Enforce an allowlist of permitted paths server-side rather than letting a client patch anything on the object. This also prevents a client from overwriting fields like id or ownerId.
Apply the test operation for concurrency, and validate the result. After applying a patch, re-validate the resulting object against your schema so a semantically valid but business-rule-violating change is caught.
import { applyPatch } from "fast-json-patch";
const FORBIDDEN = ["__proto__", "constructor", "prototype"];
function isSafe(patch) {
return patch.every(op =>
!FORBIDDEN.some(k => op.path.includes(k) || (op.from ?? "").includes(k))
);
}
if (!isSafe(incomingPatch)) throw new Error("unsafe patch path");
const result = applyPatch(structuredClone(doc), incomingPatch).newDocument;
Keeping the risk managed over time
Prototype pollution is a recurring class, not a one-time bug. New JSON-manipulation packages get the same flaw discovered in them periodically, and the pattern (__proto__ in an attacker-controlled path or key) is the same across libraries. Two habits keep you covered.
Scan your dependencies continuously. Commit your lockfile and run npm audit in CI so a fresh advisory against your patch library or any transitive JSON utility surfaces immediately. An SCA tool helps here because prototype-pollution bugs frequently live in small, deeply transitive utilities you did not choose directly. A scanner such as Safeguard can flag that a vulnerable JSON handler arrived through your dependency tree.
Keep the input-validation habit even after upgrading. The library fix protects the library; your allowlist protects your application logic and survives the next unpatched utility you pull in. For a deeper look at prototype pollution as a class, the Safeguard Academy covers the detection patterns.
FAQ
What is JSON Patch used for?
JSON Patch (RFC 6902) describes partial updates to a JSON document as a small array of operations, so a client can send only what changed instead of the whole object. It is common in REST APIs, configuration systems, and collaborative editing.
Why is JSON Patch a prototype-pollution risk?
Each operation includes a client-supplied path. If a library resolves that path without guarding against __proto__, constructor, and prototype, an attacker can write onto Object.prototype, affecting every object in the process. This affected fast-json-patch before 3.1.1 (CVE-2021-4279).
Is fast-json-patch safe now?
Version 3.1.1 and later add guards against the dangerous prototype keys, so upgrading resolves the known CVE. Still validate incoming patch paths server-side, since input validation protects your application logic beyond any single library fix.
How do I safely apply a patch from an untrusted client?
Reject paths containing __proto__, constructor, or prototype; enforce an allowlist of fields a client is permitted to patch; apply the patch to a copy; and re-validate the result against your schema before persisting it.