The 7 Critical JSON Security Risks
JSON is the most common data format in modern APIs. Understanding its security implications is critical.
1. Injection Attacks
SQL Injection via JSON
// Bad: Vulnerable - string concatenation
const query = `SELECT * FROM users WHERE data->>'name' = '${json.name}'`;
// Good: Safe - parameterized query
const query = 'SELECT * FROM users WHERE data->>\'name\' = $1';
const result = await db.query(query, [json.name]);
NoSQL Injection
// Bad: Vulnerable - accepts MongoDB operators
const filter = JSON.parse(userInput);
// If userInput is {"$gt": ""}, it matches everything!
// Good: Safe - validate structure
const filter = { name: userInput.name };
const result = await db.collection('users').find(filter);
Command Injection
// Bad: NEVER do this
eval('var data = ' + jsonString);
// Bad: Or this
new Function('return ' + jsonString)();
// Good: Always use JSON.parse
const data = JSON.parse(jsonString);
2. Prototype Pollution
Understanding the Attack
// Attacker sends:
{
"__proto__": {
"isAdmin": true
}
}
// After merge:
const user = {};
Object.assign(user, maliciousData);
console.log(user.isAdmin); // true — prototype polluted!
Prevention
// Good: Use Object.create(null) for dictionaries
const safeDict = Object.create(null);
// Good: Sanitize before merge
const SANITIZE_MAX_DEPTH = 10;
function sanitize(obj, depth = 0) {
if (depth > SANITIZE_MAX_DEPTH) {
throw new Error('Maximum depth exceeded during sanitization');
}
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
sanitized[key] = typeof value === 'object' && value !== null
? sanitize(value, depth + 1)
: value;
}
return sanitized;
}
// Good: Use Map instead of plain objects
const data = new Map();
data.set('name', 'Alice');
3. Denial of Service
Large Payloads
const MAX_BYTES = 1024 * 1024; // 1MB
app.use(express.json({ limit: MAX_BYTES }));
// Or manual check
function parseWithLimit(json) {
if (Buffer.byteLength(json, 'utf8') > MAX_BYTES) {
throw new Error('Payload too large');
}
return JSON.parse(json);
}
Deep Nesting
const MAX_DEPTH = 10;
function parseWithDepthLimit(json, maxDepth = MAX_DEPTH) {
function checkDepth(value, currentDepth) {
if (currentDepth > maxDepth) {
throw new Error('Maximum depth exceeded');
}
if (Array.isArray(value)) {
value.forEach(item => checkDepth(item, currentDepth + 1));
} else if (value && typeof value === 'object') {
Object.values(value).forEach(v => checkDepth(v, currentDepth + 1));
}
}
const data = JSON.parse(json);
checkDepth(data, 0);
return data;
}
Circular References
// Bad: This will throw
const obj = {};
obj.self = obj;
JSON.stringify(obj); // TypeError: Converting circular structure
// Good: Detect and handle
function safeStringify(obj) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
}
4. Data Leakage
Sensitive Fields
// Bad: Exposes password hash
res.json(user);
// Good: Remove sensitive fields
function sanitizeUser(user) {
const { password, salt, ...safeUser } = user;
return safeUser;
}
// Good: Or use a serialization library
class User {
toJSON() {
const { password, salt, ...safe } = this;
return safe;
}
}
Error Messages
// Bad: Exposes internal details
catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
}
// Good: Generic error message
catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal server error' });
}
5. Input Validation
Schema Validation
const Ajv = require('ajv');
const ajv = new Ajv({ allErrors: true });
const schema = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 }
},
required: ['name', 'email'],
additionalProperties: false
};
const validate = ajv.compile(schema);
function validateInput(data) {
if (!validate(data)) {
throw new Error(validate.errors.map(e => e.message).join(', '));
}
return data;
}
Type Coercion
// Bad: Dangerous - type coercion
const data = JSON.parse(userInput);
if (data.isAdmin == true) { // Loose comparison!
grantAdmin();
}
// Good: Strict comparison
if (data.isAdmin === true) {
grantAdmin();
}
// Good: Validate type explicitly
if (typeof data.isAdmin === 'boolean' && data.isAdmin === true) {
grantAdmin();
}
6. HTTPS and Transport Security
// Good: Always use HTTPS in production
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(301, `https://${req.headers.host}${req.url}`);
}
next();
});
// Good: Set security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Content-Security-Policy', "default-src 'self'");
next();
});
7. CORS Configuration
// Good: Restrict origins
const cors = require('cors');
app.use(cors({
origin: ['https://yourdomain.com'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
Security Checklist
- Validate all JSON input with schemas
- Limit payload size (1MB recommended)
- Limit nesting depth (10 levels recommended)
- Use parameterized queries for database operations
- Sanitize data before merging into objects
- Remove sensitive fields before serialization
- Use HTTPS in production
- Implement proper CORS policies
- Log security events for monitoring
- Keep dependencies updated
Related Resources
- JSON Best Practices — Secure coding conventions and data type handling
- JSON Schema Validation — Validate inputs before processing
- JavaScript JSON Guide — Safe parsing and serialization in JS
- JSON Schema Validator — Validate your JSON against schemas
- JSON Escape — Safely escape JSON strings