Secure Browser JSON: Native Parse vs Eval Pitfalls

You Pasted a Third-Party API Response Into Your JSON Viewer—And It Just Ate Your Browser Tab

It's 2:47 PM. A user reports that your web-based JSON formatter freezes when they paste a response from a partner's shipping API. You reproduce it. The tab climbs to 1.8 GB of memory, the fan spins up, and Chrome's renderer coughs up a "Page Unresponsive" dialog. The payload isn't even that large—roughly 4.2 MB. But somewhere inside that blob is a deeply nested object, a prototype-polluting key, and a string field carrying an inline `<script>` tag that your viewer dutifully rendered into the DOM.

This is the single most common pain point for developers building browser-based JSON tools: you trust the data because it looks like JSON. But untrusted third-party JSON is never just JSON. It's a delivery vehicle for memory exhaustion, prototype pollution, and DOM-based XSS—all hiding behind a innocent-looking `application/json` content type.

Let's walk through where things break, why they break, and exactly how to fix them.

The Problem: Three Silent Killers in Untrusted JSON

When your JSON tool accepts arbitrary input from a third-party source—a webhook, a partner API, a user paste—you're ingesting data you did not produce and cannot vouch for. Most browser-based parsers handle this with a naive `JSON.parse()` and a recursive render. That approach fails in three specific ways:

1. Memory Exhaustion via Deep Nesting

`JSON.parse` itself is surprisingly resilient. V8 caps recursion depth and throws a clean `RangeError` on pathological nesting. The real danger is your renderer. If you walk the parsed object recursively to build a collapsible tree view, a payload nested 10,000 levels deep will blow the call stack. A payload with 50,000 sibling keys at one level won't crash—but it'll create 50,000 DOM nodes and lock the thread for 30+ seconds.

2. Prototype Pollution via `__proto__` and `constructor`

Here's a payload that looks harmless:

{
  "__proto__": {
    "isAdmin": true
  }
}

If your tool merges parsed JSON into an existing config object using a shallow or deep merge that doesn't guard against `__proto__`, `constructor`, or `prototype` keys, you've just mutated `Object.prototype`. Every object in your app now has `isAdmin: true`. In a JSON viewer context, this is less catastrophic than in a backend—but if your tool persists settings or shares state with a parent application, pollution leaks outward.

3. XSS via String Values Rendered as HTML

A third-party API returns a product description field containing:

"description": "<img src=x onerror=alert(document.cookie)>"

If your tree viewer inserts string values using `innerHTML` instead of `textContent`, that payload executes. This is the most common and most dangerous failure mode in browser-based JSON tools. One `innerHTML` assignment in a render function is all it takes.

The Cause: Why Standard Parsing Patterns Fail

The root cause is almost always the same: developers treat JSON parsing as a one-step operation (`JSON.parse`) when it's actually a three-step pipeline—parse, sanitize, render—and steps two and three get skipped.

The Naive Pipeline That Breaks

Most JSON tool code looks like this:

const data = JSON.parse(input);
renderTree(data, container); // recursive, innerHTML-based

`JSON.parse` produces a plain object. It does not strip dangerous keys. It does not limit depth. It does not escape string values for DOM insertion. It just deserializes. Every security boundary must be added after parsing and before rendering—and that's the gap attackers exploit.

Why `JSON.parse` Alone Isn't a Security Boundary

`JSON.parse` is safe against code injection—it won't execute arbitrary JavaScript the way `eval` does. That's true. But it provides zero guarantees about the shape of the resulting object. A 4 MB JSON string can produce an object graph with 120,000 nodes. A key named `__proto__` parses without complaint. A string value containing HTML markup is just a string to the parser—it only becomes dangerous when your code puts it somewhere the browser interprets as HTML.

The Fix: A Secure Three-Stage Pipeline

Here's how to harden your JSON tool against all three attack vectors, stage by stage.

Stage 1: Controlled Parsing with Depth and Size Limits

Before you even call `JSON.parse`, enforce a size limit on the raw input. 10 MB is a reasonable ceiling for a browser-based viewer—anything beyond that should be rejected with a clear message, not silently attempted.

After parsing, validate the depth before rendering. Walk the object iteratively (not recursively) with a stack-based approach, and reject anything deeper than a sane threshold—64 levels is generous for real-world API responses while preventing stack overflow:

function getMaxDepth(obj) {
  let maxDepth = 0;
  const stack = [{ value: obj, depth: 1 }];
  while (stack.length) {
    const { value, depth } = stack.pop();
    if (depth > 64) {
      throw new Error('JSON nesting exceeds safe depth of 64');
    }
    maxDepth = Math.max(maxDepth, depth);
    if (value && typeof value === 'object') {
      for (const key of Object.keys(value)) {
        stack.push({ value: value[key], depth: depth + 1 });
      }
    }
  }
  return maxDepth;
}

For sibling count, cap the number of keys rendered per level. If an object has more than 1,000 keys, render the first 100 and show a "1,000+ keys collapsed for performance" notice. This keeps DOM node count bounded.

Stage 2: Sanitizing Dangerous Keys

After parsing, strip or rename prototype-polluting keys before the object touches any merge logic or renderer. Use `Object.create(null)` as your base where possible—it has no prototype, so `__proto__` assignments are inert:

function sanitize(obj) {
  if (typeof obj !== 'object' || obj === null) return obj;

  const clean = Array.isArray(obj) ? [] : Object.create(null);
  const dangerous = ['__proto__', 'constructor', 'prototype'];

  for (const key of Object.keys(obj)) {
    if (dangerous.includes(key)) continue;
    clean[key] = sanitize(obj[key]);
  }
  return clean;
}

By building on `Object.create(null)`, even if a future code path attempts a merge, there's no `Object.prototype` to pollute. This is a small change with an outsized security payoff.

Stage 3: Safe Rendering

This is where most tools fail. The rule is simple and absolute: never use `innerHTML` for JSON string values. Use `textContent` for every value node, or use `document.createTextNode()` explicitly:

function renderValue(value, container) {
  const span = document.createElement('span');
  span.textContent = String(value); // safe—browser treats this as text, not HTML
  span.className = 'json-value';
  container.appendChild(span);
}

If your tool offers a "raw preview" mode that shows the original JSON string in a `<pre>` block, the same rule applies. Set `.textContent`, not `.innerHTML`. The one exception is if you run the string through a dedicated syntax highlighter that produces escaped HTML—and even then, verify the highlighter escapes `<`, `>`, and `&` before wrapping tokens in `<span>` tags.

Handling Edge Cases That Trip Up Even Hardened Tools

Unicode and Surrogate Pairs

Third-party JSON sometimes contains malformed Unicode escapes like `\uD800` (a lone surrogate). `JSON.parse` handles these inconsistently across engines. In modern V8, lone surrogates are preserved in the string, which is fine for display—but if your tool re-serializes the data with `JSON.stringify`, the output may differ from the input. Flag this to users rather than silently altering their data.

Large Numbers Beyond `Number.MAX_SAFE_INTEGER`

A financial API returns `"transactionId": 9007199254740993`. `JSON.parse` silently loses precision—it becomes `9007199254740992`. For a JSON viewer, this is a data integrity issue, not a security issue, but it erodes user trust. If your tool targets developers working with financial or blockchain APIs, consider parsing with a reviver function that detects large integers and preserves them as strings:

const data = JSON.parse(input, (key, value) => {
  if (typeof value === 'number' && !Number.isSafeInteger(value)) {
    return String(value);
  }
  return value;
});

Re-Serialization Fidelity

If your JSON formatter reformats input and users copy the output back into their code, silent changes—key reordering, precision loss, stripped dangerous keys—can break their pipelines. Always show a diff or a warning when the output is not byte-identical to the input. This is a trust feature, not a nice-to-have.

The Secure Pipeline, Summarized

Your final flow should look like this:

1. **Size gate**: Reject input over 10 MB before parsing. 2. **Parse**: Use `JSON.parse` with a reviver for large-number handling. 3. **Depth check**: Iteratively verify nesting doesn't exceed 64 levels. 4. **Sanitize**: Strip `__proto__`, `constructor`, `prototype` keys; build on `Object.create(null)`. 5. **Render**: Use `textContent` exclusively for value insertion; cap DOM nodes at 100 per level. 6. **Re-serialize safely**: Warn on any transformation that changes the original data.

That 4.2 MB shipping API payload that crashed your tab at 2:47 PM? With this pipeline, it parses in under 90 milliseconds, renders without a single `innerHTML` call, and the `<script>` tag inside that description field shows up as harmless gray text in your tree view—exactly where it belongs.

The difference between a JSON tool that survives contact with untrusted data and one that doesn't isn't a single line of code. It's the discipline of treating parse, sanitize, and render as three separate concerns, each with its own boundary checks. Skip any one of the three, and you're one paste away from a frozen tab.

Frequently Asked Questions

Is JSON.parse() safe for untrusted third-party data?

Yes, the native JSON.parse() method is generally safe because it only parses data and does not execute JavaScript code. However, you should always wrap it in a try...catch block to prevent application crashes from malformed JSON payloads.

Can parsing JSON in the browser cause XSS vulnerabilities?

Parsing JSON itself does not cause XSS, but insecurely rendering the parsed data into the DOM can. Always use textContent or frameworks that automatically escape HTML when displaying parsed JSON values to prevent Cross-Site Scripting attacks.

How do I prevent prototype pollution when parsing JSON?

Prototype pollution can occur if an attacker injects keys like __proto__ into the JSON payload. To prevent this, avoid recursively merging parsed JSON into existing objects, or use utility libraries that explicitly block prototype-modifying keys.

What is the safest way to parse JSON from a third-party API?

The safest approach is to use the native JSON.parse() method inside a try...catch block and validate the resulting object structure against a schema. Using a JSON schema validation library ensures the third-party data matches your expected types and formats before use.

Does JSON.parse() execute JavaScript code?

No, JSON.parse() is a strict parser that converts JSON text into a JavaScript object without evaluating it as code. This is why it is much safer than the deprecated eval() function, which historically executed arbitrary JavaScript embedded in JSON strings.

How to safely display formatted JSON on a webpage?

To safely display formatted JSON, use JSON.stringify() to create the string representation and inject it using the textContent property of a DOM element. Never use innerHTML to display JSON data, as malicious payloads could contain executable HTML or script tags.

How can I handle malicious or malformed JSON payloads?

Always wrap your parsing logic in a try...catch block to gracefully handle malformed JSON without breaking your application. Additionally, implement strict input validation and limit the size of the incoming JSON string to protect against denial-of-service attacks.

Are there alternative JSON parsers that are more secure?

For standard browser environments, the native JSON.parse() is the fastest and most secure option available. Alternative libraries are generally only needed if you require specific features like JSON5 support, but they must be carefully audited as they do not have the same native security guarantees.

Why should I never use eval() to parse JSON?

Using eval() to parse JSON allows arbitrary JavaScript code within the string to be executed, leading to severe security vulnerabilities like Cross-Site Scripting (XSS). Always use JSON.parse() instead, as it safely interprets data without executing scripts.

How to securely format large JSON objects in the browser?

When formatting large JSON objects, using JSON.stringify() with indentation can cause significant memory and performance issues. To securely handle large third-party datasets, parse and format the data in chunks or use a Web Worker to keep the main browser thread responsive.