Attack Surface Overview
1. Prototype Pollution
The most dangerous JSON-specific vulnerability. An attacker sends a payload with __proto__ keys that modify the global Object prototype.
The Attack
1// Attacker sends this JSON body:2const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');34// Vulnerable merge function5function merge(target, source) {6 for (const key in source) {7 if (typeof source[key] === 'object') {8 target[key] = target[key] || {};9 merge(target[key], source[key]);10 } else {11 target[key] = source[key];12 }13 }14}1516const config = {};17merge(config, malicious);1819// Now EVERY object in the app is "admin"20const user = {};21console.log(user.isAdmin); // true — pollution succeededThe Fix
1function safeMerge(target, source) {2 for (const key of Object.keys(source)) {3 // Block prototype pollution vectors4 if (key === '__proto__' || key === 'constructor' || key === 'prototype') {5 continue;6 }7 if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {8 target[key] = target[key] || Object.create(null);9 safeMerge(target[key], source[key]);10 } else {11 target[key] = source[key];12 }13 }14 return target;15}1617// Or use Object.create(null) for prototype-free objects18const safeObj = Object.create(null); // no __proto__ at allLibraries Affected
2. Mass Assignment
When you directly spread or assign JSON request bodies into database models, an attacker can overwrite fields they shouldn't have access to.
1// POST /api/users — attacker sends:2// { "name": "Evil", "role": "admin", "verified": true }34app.post('/api/users', async (req, res) => {5 // BAD: trusting every field from the request6 const user = await db.users.create(req.body);7});1app.post('/api/users', async (req, res) => {2 // GOOD: only pick allowed fields3 const { name, email } = req.body;4 const user = await db.users.create({5 name,6 email,7 role: 'user', // hardcoded default8 verified: false, // never trust input9 });10});3. XSS via JSON in HTML
Embedding JSON in HTML script tags or innerHTML without escaping allows script injection.
1// Server-side rendering danger2const userData = { bio: '</script><script>alert("xss")</script>' };34// BAD: raw embedding5res.send(`<script>const user = ${JSON.stringify(userData)}</script>`);6// Renders: </script><script>alert("xss")</script>1function safeJsonForHtml(data) {2 return JSON.stringify(data)3 .replace(/</g, '\u003c')4 .replace(/>/g, '\u003e')5 .replace(/&/g, '\u0026')6 .replace(/'/g, '\u0027');7}89// Safe embedding10res.send(`<script>const user = ${safeJsonForHtml(userData)}</script>`);4. Never Use eval() for JSON
1// NEVER DO THIS2const data = eval('(' + jsonString + ')');3// Attacker can inject: '); process.exit(); //4// This runs arbitrary code on your server56// ALWAYS DO THIS7const data = JSON.parse(jsonString);8// Only accepts valid JSON — no code execution5. Schema Validation as Defense
JSON Schema validation is your strongest defense layer. It rejects unexpected fields, wrong types, and malicious patterns before they reach your business logic.
1import Ajv from 'ajv';2const ajv = new Ajv({ allErrors: true, removeAdditional: true });34const userSchema = {5 type: 'object',6 properties: {7 name: { type: 'string', minLength: 1, maxLength: 100 },8 email: { type: 'string', format: 'email' },9 },10 required: ['name', 'email'],11 additionalProperties: false, // rejects __proto__, role, etc.12};1314const validate = ajv.compile(userSchema);1516function validateInput(body) {17 if (!validate(body)) {18 return { valid: false, errors: validate.errors };19 }20 return { valid: true, data: body };21}Tip
additionalProperties: false combined with removeAdditional: true strips unknown fields and rejects __proto__ / constructor keys. Use our JSON Schema Validator to test your schemas.Security Checklist
Try These Tools
Try It — Is This JSON Safe?
This JSON contains a prototype pollution attempt. Our validator will tell you it's valid JSON — but it's not safe to merge blindly.
Try It Yourself
Valid JSON — but extremely dangerous if merged naively