JSON Security Best Practices: Protecting Data in Web Applications
JSON has become the standard format for data exchange in modern web applications, but with its widespread adoption comes increased security risks. From injection attacks to data exposure, JSON-related vulnerabilities can compromise entire systems if not properly addressed.
This comprehensive guide covers essential security practices for working with JSON in web applications, including prevention techniques, validation strategies, and secure implementation patterns.
Common JSON Security Vulnerabilities
JSON Injection Attacks
JSON injection occurs when malicious data is inserted into JSON structures, potentially leading to code execution or data corruption:
// Vulnerable code - directly embedding user input
const userInput = req.body.comment; // Could be: "}; alert('XSS'); //"
const jsonData = `{"comment": "${userInput}"}`; // Dangerous!
// Safe approach - always use proper JSON serialization
const safeJson = JSON.stringify({
comment: userInput // JSON.stringify handles escaping automatically
});
Cross-Site Scripting (XSS) Through JSON
XSS can occur when JSON data is improperly rendered in HTML contexts:
// Vulnerable - direct insertion into HTML
const jsonData = {"message": "<script>alert('XSS')</script>"};
document.getElementById('content').innerHTML = jsonData.message; // Dangerous!
// Safe - proper escaping or using textContent
document.getElementById('content').textContent = jsonData.message;
// Or using a templating engine with auto-escaping
// {{jsonData.message}} in Handlebars or similar
Prototype Pollution
Prototype pollution is a vulnerability where attackers can modify object prototypes through malicious JSON data:
// Vulnerable code
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attack payload: {"__proto__": {"isAdmin": true}}
// This could grant unauthorized admin privileges!
Safe implementation:
// Safe merge function
function safeMerge(target, source) {
for (let key in source) {
// Prevent prototype pollution
if (key === '__proto__' || key === 'constructor') continue;
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
Input Validation and Sanitization
Server-Side Validation
Always validate JSON data on the server side, regardless of client-side validation:
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120),
preferences: Joi.object({
notifications: Joi.boolean(),
theme: Joi.string().valid('light', 'dark')
})
});
app.post('/api/users', (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: 'Validation failed', details: error.details });
}
// Process validated data
createUser(value);
res.json({ success: true });
});
Client-Side Sanitization
Sanitize JSON data before rendering in UI components:
import DOMPurify from 'dompurify';
function renderUserData(userData) {
const sanitizedData = {
...userData,
bio: DOMPurify.sanitize(userData.bio || ''),
profile: {
...userData.profile,
description: DOMPurify.sanitize(userData.profile?.description || '')
}
};
document.getElementById('user-bio').textContent = sanitizedData.bio;
}
Secure JSON Parsing
Preventing Prototype Pollution in Parsing
function safeJSONParse(str) {
try {
const parsed = JSON.parse(str);
if (parsed && typeof parsed === 'object') {
if ('__proto__' in parsed || 'constructor' in parsed) {
throw new Error('Potential prototype pollution detected');
}
}
return parsed;
} catch (error) {
console.error('JSON parsing error:', error);
return null;
}
}
Handling Malformed JSON
app.post('/api/data', (req, res) => {
const data = safeJSONParse(req.body);
if (data === null) {
return res.status(400).json({ error: 'Invalid JSON format' });
}
try {
processData(data);
res.json({ success: true });
} catch (error) {
console.error('Data processing error:', error);
res.status(500).json({ error: 'Data processing failed' });
}
});
Authentication and Authorization
Secure API Endpoints
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Access token required' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
req.user = user;
next();
});
}
app.post('/api/secure-data', authenticateToken, (req, res) => {
const secureData = getSecureData(req.user.id);
res.json(secureData);
});
Rate Limiting
const rateLimit = require('express-rate-limit');
const jsonApiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: 'Too many requests, please try again later.' }
});
app.use('/api/', jsonApiLimiter);
Data Protection and Privacy
Sensitive Data Handling
// Exposing sensitive data - bad
const userResponse = { id: user.id, username: user.username, email: user.email, password: user.password };
// Safe response
const safeResponse = { id: user.id, username: user.username, email: user.email };
Data Encryption
const crypto = require('crypto');
function encryptSensitiveData(data) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'GfG', 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
encrypted += cipher.final('hex');
return { data: encrypted, iv: iv.toString('hex') };
}
function decryptSensitiveData(encryptedData) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'GfG', 32);
const iv = Buffer.from(encryptedData.iv, 'hex');
const decipher = crypto.createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
Cross-Origin Resource Sharing (CORS)
const cors = require('cors');
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];
if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
callback(new Error('Not allowed by CORS'));
},
credentials: true
};
app.use(cors(corsOptions));
Content Security Policy (CSP)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self'; object-src 'none'; frame-src 'none'; child-src 'none'"
);
next();
});
JSON Web Tokens (JWT) Security
function generateSecureToken(user) {
const payload = { userId: user.id, username: user.username };
return jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '24h' });
}
function verifyToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
} catch (error) {
throw new Error('Invalid token');
}
}
Tools for JSON Security
- Validate JSON: JSON Validator
- Compare JSON files: JSON Compare
- Inspect JWT tokens: JWT Debugger
Best Practices Summary
- Always validate input
- Prevent injection attacks
- Protect against prototype pollution
- Implement authentication and authorization
- Never expose sensitive data
- Use HTTPS
- Implement rate limiting
- Configure CORS properly
- Use Content Security Policy
- Monitor and log
Conclusion
JSON security is critical for modern web development. Regularly audit your JSON handling code, apply these best practices, and stay updated on vulnerabilities to protect your applications and user data.