JSON Tools

JSON Security: Injection & Prototype Pollution Prevention

The Debuggers Engineering Team
11 min read

TL;DR

  • JSON injection happens when untrusted input is concatenated into JSON strings instead of using JSON.stringify()
  • Prototype pollution lets attackers modify Object.prototype through malicious __proto__ keys in parsed JSON
  • Deeply nested JSON (10,000+ levels) crashes parsers and can be used for denial-of-service attacks
  • Always validate, sanitise, and limit the depth and size of incoming JSON before processing it

Table of Contents

Security shield protecting data flow with encrypted connections

JSON Injection

JSON injection occurs when user-supplied data is concatenated into a JSON string without proper escaping.

Vulnerable code:

// DANGEROUS: string concatenation
const json = '{"username": "' + userInput + '"}';

If userInput is admin", "role": "superadmin, the resulting JSON becomes:

{"username": "admin", "role": "superadmin"}

The attacker injected an additional field. If the server processes this JSON and trusts the role field, the attacker gains elevated privileges.

The fix is trivial: always use JSON.stringify():

// SAFE: JSON.stringify handles escaping
const data = { username: userInput };
const json = JSON.stringify(data);

JSON.stringify() automatically escapes special characters, making injection impossible. Never build JSON strings manually.

Database-level injection: NoSQL databases like MongoDB accept JSON-like query objects. If user input flows into query construction:

// DANGEROUS: MongoDB query injection
const query = JSON.parse(`{"username": "${userInput}"}`);
db.users.findOne(query);

If userInput is {"$gt": ""}, the query becomes {"username": {"$gt": ""}}, which matches all users. Always use parameterised queries or whitelist-validate input.

Prototype Pollution

Prototype pollution is one of the most dangerous JSON-specific attacks. It exploits how JavaScript handles object property lookups through the prototype chain.

The attack:

const maliciousJson = '{"__proto__": {"isAdmin": true}}';
const parsed = JSON.parse(maliciousJson);

// Now EVERY object in the application has isAdmin = true
const user = {};
console.log(user.isAdmin); // true (polluted!)

This works because JSON.parse creates an object with a __proto__ key. When JavaScript assigns __proto__, it modifies the prototype of Object, affecting every object created afterward.

Real-world impact: If any authorisation check does if (user.isAdmin), the attacker bypasses it for every user in the system.

Prevention:

// Method 1: Strip dangerous keys during parsing
function safeParse(json) {
  return JSON.parse(json, (key, value) => {
    if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
      return undefined; // Remove the key
    }
    return value;
  });
}

// Method 2: Create objects without prototype
function safeParseNoProto(json) {
  const parsed = JSON.parse(json);
  return Object.assign(Object.create(null), parsed);
}

// Method 3: Use Object.freeze on prototypes
Object.freeze(Object.prototype);
// WARNING: This breaks many libraries - use with caution

Method 1 (reviver function) is the recommended approach because it is compatible with all libraries and handles nested __proto__ keys.

Code showing security vulnerability detection and prevention patterns

Denial of Service via Deep Nesting

JSON parsers use recursion to handle nested objects and arrays. An attacker can send JSON with thousands of nesting levels to crash the parser:

# Generate malicious JSON with 100,000 nesting levels
malicious = '{"a":' * 100000 + '1' + '}' * 100000

When JSON.parse() processes this, it recursively descends 100,000 levels, exceeding the maximum call stack size and crashing the Node.js process.

Prevention:

function parseWithDepthLimit(json, maxDepth = 100) {
  let depth = 0;
  let maxReached = 0;
  
  for (const char of json) {
    if (char === '{' || char === '[') {
      depth++;
      maxReached = Math.max(maxReached, depth);
      if (depth > maxDepth) {
        throw new Error(`JSON exceeds maximum nesting depth of ${maxDepth}`);
      }
    } else if (char === '}' || char === ']') {
      depth--;
    }
  }
  
  return JSON.parse(json);
}

Also set maximum request body size on your web server:

// Express.js
app.use(express.json({ limit: '1mb' })); // Reject bodies over 1MB

Mass Assignment Vulnerabilities

Mass assignment happens when an API accepts a full JSON object and applies all fields to a database record without filtering:

// DANGEROUS: updates all fields from user input
app.put('/api/users/:id', async (req, res) => {
  await db.users.update(req.params.id, req.body);
});

An attacker sends:

{
  "name": "Normal Name",
  "role": "admin",
  "isVerified": true,
  "billingPlan": "enterprise"
}

The server happily updates all four fields, giving the attacker admin privileges and a free enterprise plan.

Prevention: always whitelist allowed fields:

app.put('/api/users/:id', async (req, res) => {
  const allowed = ['name', 'email', 'avatar'];
  const updates = {};
  
  for (const key of allowed) {
    if (req.body[key] !== undefined) {
      updates[key] = req.body[key];
    }
  }
  
  await db.users.update(req.params.id, updates);
});

Or use Zod schemas (recommended) to strip unknown fields automatically:

const UpdateUserSchema = z.object({
  name: z.string().optional(),
  email: z.string().email().optional(),
  avatar: z.string().url().optional(),
}).strict(); // Rejects unknown fields

const validated = UpdateUserSchema.parse(req.body);

Server-Side Request Forgery via JSON

If your API processes URLs from JSON input (webhook URLs, image URLs, redirect targets), an attacker can point them to internal services:

{
  "webhookUrl": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}

This URL targets the AWS metadata service, exposing IAM credentials. Always validate URLs against an allowlist of domains and block private IP ranges.

Secure JSON Parsing Patterns

The complete secure parsing function:

interface ParseOptions {
  maxSize?: number;      // Maximum JSON string length in bytes
  maxDepth?: number;     // Maximum nesting depth
  stripProto?: boolean;  // Remove __proto__ keys
}

function secureParse(json: string, options: ParseOptions = {}): unknown {
  const { maxSize = 1_000_000, maxDepth = 50, stripProto = true } = options;
  
  // Size check
  if (json.length > maxSize) {
    throw new Error(`JSON exceeds maximum size of ${maxSize} bytes`);
  }
  
  // Depth check
  let depth = 0;
  for (const char of json) {
    if (char === '{' || char === '[') {
      depth++;
      if (depth > maxDepth) {
        throw new Error(`JSON exceeds maximum depth of ${maxDepth}`);
      }
    } else if (char === '}' || char === ']') {
      depth--;
    }
  }
  
  // Parse with optional prototype stripping
  if (stripProto) {
    return JSON.parse(json, (key, value) => {
      if (key === '__proto__' || key === 'constructor') return undefined;
      return value;
    });
  }
  
  return JSON.parse(json);
}

Use our JSON Formatter to inspect suspicious JSON payloads. The tree view makes it easy to spot unexpected keys like __proto__ or constructor. For testing your API endpoints against these attack patterns, use our API Request Tester.

For the complete JSON developer reference, see our pillar guide: The Complete JSON Guide for Web Developers.

For safe API response handling patterns, see: How to Handle JSON API Responses Safely.

The Debuggers provides security-focused development services, including API security audits and secure coding practices.

Security Checklist

CheckHow
Never build JSON with string concatenationUse JSON.stringify()
Protect against prototype pollutionUse a reviver function that strips __proto__
Limit request body sizeSet express.json({ limit: '1mb' })
Limit nesting depthCheck depth before parsing
Prevent mass assignmentWhitelist allowed fields or use strict Zod schemas
Validate URLs in JSONAllowlist domains, block private IP ranges
Hash sensitive data before storingUse our Hash Generator

Security audit results showing vulnerability scan status

Frequently Asked Questions

Is JSON.parse() safe to use with untrusted input?

JSON.parse() itself does not execute code, so it is safe from code injection. However, it is vulnerable to prototype pollution (via __proto__ keys), denial of service (via deep nesting), and it does not validate data shape. Always use a reviver function to strip prototype keys and validate the parsed data against a schema.

How do I protect a Node.js API from JSON-based attacks?

Set a request body size limit (1MB is reasonable for most APIs), validate all JSON input against Zod or Ajv schemas with strict mode (reject unknown fields), use a reviver function in JSON.parse to strip __proto__ keys, and never pass raw request body fields to database update operations. These four measures prevent the majority of JSON-based attacks.

Can JSON contain executable code?

No. Unlike YAML (which can deserialise objects and call constructors), JSON only supports strings, numbers, booleans, null, arrays, and objects. It cannot contain functions, class instances, or executable code. However, if you use eval() to parse JSON (never do this), then arbitrary code execution becomes possible. Always use JSON.parse().

What is the difference between JSON injection and SQL injection?

SQL injection exploits string concatenation in SQL queries to execute arbitrary database commands. JSON injection exploits string concatenation in JSON construction to insert additional fields or modify the structure. The prevention is the same concept: use parameterised/structured methods (prepared statements for SQL, JSON.stringify for JSON) instead of string concatenation.


Inspect suspicious JSON payloads

Use our free JSON Formatter to visualise JSON structure and spot unexpected keys. Hash sensitive data with our Hash Generator.

Need a security audit for your API? The Debuggers provides security-focused development and code review services.

Need Help Implementing This in a Real Project?

Our team supports end-to-end development for web and mobile software, from architecture to launch.

json securityjson injectionprototype pollutionjson vulnerabilityjson security best practices

Found this helpful?

Join thousands of developers using our tools to write better code, faster.