JSON Security: Common Vulnerabilities and How to Avoid Them
JSON carries no executable code, and it avoids XML's external entity risks and YAML's type coercion surprises, so it has a reputation as safe to parse. The parsing step mostly earns that reputation. The vulnerabilities come from what your code does with the parsed object afterward.
Prototype pollution via JSON input
JavaScript objects inherit from a shared prototype chain. If untrusted JSON contains a key like "__proto__" and your code merges it into an existing object without guarding special keys, an attacker can inject properties onto Object.prototype itself. That affects every object in your application, beyond the one receiving the merge.
{
"__proto__": {
"isAdmin": true
}
}Deep-merge this into a user object with a recursive merge that doesn't special-case __proto__, constructor, or prototype keys, and every object in the process, including ones that never touched this payload, can gain an isAdmin property set to true. Attackers have exploited this vulnerability class in several popular npm merge/extend libraries.
How to avoid prototype pollution
Use Object.create(null) for maps built from untrusted keys, since it has no prototype for pollution to reach. Prefer Map over plain objects when the keys come from user input and you don't need to serialize the result as JSON. If you must deep-merge untrusted JSON into an existing object, use a merge utility that blocks __proto__, constructor, and prototype as keys, and check whether your dependency has patched this (most major libraries have in recent versions) instead of assuming. Keep dependencies current: maintainers have patched this vulnerability class many times across popular packages as researchers found new bypass techniques.
Resource exhaustion: size and depth limits
Parsing untrusted JSON costs CPU and memory in proportion to the input, so put a size limit at the door first. In Express, `app.use(express.json({ limit: "1mb" }))` rejects oversized bodies before parsing, and every framework has an equivalent. Without a limit, a client that uploads a 2 GB body gets to allocate 2 GB per request, and the parsed object usually takes several times more memory than its text.
Depth is the second axis. V8's JSON parser is iterative and survives nesting far beyond any legitimate payload, but the code that walks the result usually recurses. A recursive validator, serializer, or template renderer over a document nested tens of thousands of levels deep will overflow the stack (RangeError: Maximum call stack size exceeded), and JSON.stringify recurses too and throws on deep enough structures. Schema validators can cap depth (Ajv and Zod both support limits), and a depth check before any recursive traversal costs little. The attack payload stays small: `[` repeated 100,000 times weighs about 100 KB of request body.
Insecure deserialization beyond plain JSON.parse
Plain JSON.parse() produces plain data: strings, numbers, booleans, null, plain objects, and arrays. It can't construct arbitrary class instances or execute code, unlike some other serialization formats (Python's pickle can execute arbitrary code during deserialization). The risk appears when a reviver function or a downstream step reconstructs class instances or runs logic based on untrusted field values. One example: code that reads a "type" field from the JSON and uses it to require() a module or dispatch to a handler by name without checking the string against an allowlist.
Embedding JSON in HTML: the </script> problem
A server-rendered page that embeds JSON (hydration data, feature flags, preloaded state) inside a <script> tag has a subtle escape hatch: the HTML parser doesn't know JSON syntax. It scans for </script> and ends the tag there, even when that sequence sits inside a JSON string value. A user whose display name is `</script><script>alert(1)</script>` can break out of your data island into executable context.
<script id="__DATA__" type="application/json">
{ "displayName": "</script><script>alert(1)</script>" }
</script>Escape the payload for the HTML script context: replace < with `\u003c`, > with `\u003e`, & with `\u0026`, and escape the U+2028/U+2029 line separators, which older JavaScript parsers treat as literal newlines. JSON strings accept all of those escapes and JSON.parse decodes them, so the data round-trips unchanged and the HTML parser can't be tricked. Most frameworks' serialization helpers do this for you. The bug appears when someone hand-rolls JSON.stringify into a template literal.
Validate against a schema before trusting untrusted JSON
JSON.parse() guarantees syntactic validity and nothing about whether the data matches the shape your code expects. A request body that's valid JSON but missing a required field, or carrying a string where you expected a number, can crash downstream code or produce wrong behavior with no error. Validate incoming JSON against a schema (JSON Schema, Zod, Joi, or similar) before using it. You catch malformed structure at the boundary, reject it with a useful error message instead of a confusing crash three functions deep, and the schema documents what shape the endpoint expects.
Secrets, logs, and pasting payloads into tools
JSON payloads carry secrets all the time: auth tokens echoed into logs, personal data in user records, connection strings in config dumps. Three habits cover most of it. Redact before logging: serialize an allowlist of fields, never the raw request body. Scan any JSON you're about to commit (fixtures, samples, bug repros) for secrets, as you'd scan for hardcoded API keys. And before pasting production payloads into a web tool, check where it processes data. Browser-side tools parse locally, while server-side ones send your customers' data to whatever backs them.
A practical checklist
Validate untrusted JSON against a schema at the boundary, before it reaches business logic. Don't deep-merge untrusted JSON into long-lived objects without a merge utility that blocks dangerous keys. Avoid reviver functions or dynamic dispatch based on untrusted string fields unless you check the values against an allowlist. Keep JSON-handling dependencies (parsers, merge utilities, schema validators) up to date, because researchers keep finding new variants of these bugs. Set size limits on JSON request bodies to reduce exposure to resource-exhaustion attacks via deeply nested or huge payloads.
FAQ
- Is JSON.parse() itself vulnerable to code execution, like eval()?
- No. JSON.parse() produces plain data structures (strings, numbers, booleans, null, plain objects, arrays) and never executes code. Some very old codebases parsed JSON with eval() before native JSON.parse() became standard, and that did execute code. Modern JSON.parse() can't run code; the risks in this article come from what your code does with the parsed result.
- What is prototype pollution in simple terms?
- An attacker sends input (often JSON with a __proto__ key), and code that doesn't guard special property names merges it into an object. The attacker can then add or overwrite properties on the shared base object that all JavaScript objects inherit from, which affects the whole application.
- Do I need schema validation if I already use TypeScript?
- Yes. The TypeScript compiler erases types, so they give zero runtime protection. Nothing checks a malformed or malicious JSON payload arriving over HTTP unless you validate it with a library like Zod, Joi, or a JSON Schema validator. TypeScript stops you from writing code that mishandles the expected shape; it can't verify that the incoming data matches that shape.
- Are YAML or XML safer than JSON to parse?
- No, they have their own vulnerability classes. YAML parsers in some languages support type coercion and tags that can construct arbitrary objects unless you configure a restricted "safe load" mode. XML parsers fall to XML External Entity (XXE) attacks if you leave entity expansion on. JSON's simpler type system (no tags, no entities) removes those attack surfaces, but the deserialization and merging risks covered here still apply.
- Is it safe to paste production JSON into an online formatter or validator?
- It depends on where the tool processes your data. Browser-side tools parse locally and transmit nothing; server-side ones send the payload to whatever backs them. Look for "client-side" or "in your browser" processing before pasting anything with tokens, personal data, or production payloads. Once the data reaches someone else's server, you've lost control of it.
Try these tools
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.