Why JSON Security Matters
JSON is the backbone of almost every web API, which makes it a high-value attack surface. Insecure JSON handling causes real breaches: deserialization vulnerabilities, prototype pollution, injection attacks, and information leakage. This guide explains each risk and how to prevent it. Use the JSON Schema Validator to enforce schema rules on any incoming JSON payload.
1. JSON Injection in SQL Queries
Never concatenate JSON values directly into SQL strings:
// UNSAFE — SQL injection
const body = JSON.parse(req.body);
const query = `SELECT * FROM users WHERE name = '${body.name}'`;
// If body.name is: '; DROP TABLE users; --
// The query becomes: SELECT * FROM users WHERE name = ''; DROP TABLE users; --Fix: Use parameterized queries:
// SAFE — parameterized query
const { rows } = await pool.query(
"SELECT * FROM users WHERE name = $1",
[body.name]
);Never interpolate JSON values into raw SQL, shell commands, OS paths, LDAP queries, or XPath expressions.
2. JSON Injection in MongoDB (NoSQL Injection)
MongoDB operators like $gt, $where can be injected via JSON:
// UNSAFE — attacker sends: { "username": { "$gt": "" } }
const user = await User.findOne({ username: req.body.username });
// "$gt": "" matches ALL users — authentication bypassed!Fix: Sanitize operator keys:
// Option 1: use express-mongo-sanitize
const sanitize = require("express-mongo-sanitize");
app.use(sanitize()); // strips keys that start with $
// Option 2: explicitly pick known-safe fields
const { username, password } = req.body;
const user = await User.findOne({
username: String(username), // force string type
password: String(password),
});3. Prototype Pollution
Prototype pollution occurs when an attacker sends a JSON payload that modifies Object.prototype, affecting all objects in your application:
// Attacker payload:
// { "__proto__": { "isAdmin": true } }
const data = JSON.parse(userInput);
Object.assign({}, data); // pollutes Object.prototype.isAdmin = true!
// Later in your code:
const user = {};
console.log(user.isAdmin); // true — security bypass!Fixes:
// Option 1: use Object.create(null) to create objects without prototype
const safe = Object.assign(Object.create(null), JSON.parse(input));
// Option 2: use a safe merge library
const deepmerge = require("deepmerge");
// Option 3: validate with JSON Schema (reject unknown keys)
const schema = { type: "object", additionalProperties: false, ... };
validate(schema, data);
// Option 4: use the safe-flat or qs sanitize librariesDetection: Check for the pattern {"__proto__": or {"constructor": in incoming JSON.
4. Insecure Deserialization
Some languages serialize object metadata into JSON, which can be exploited to execute arbitrary code when deserialized. Java (Jackson), Python (pickle), and PHP (unserialize) are classic examples.
Fix for Java/Jackson:
// UNSAFE — polymorphic type handling enabled
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // dangerous! allows arbitrary class instantiation
// SAFE — use specific type references only
ObjectMapper mapper = new ObjectMapper();
mapper.deactivateDefaultTyping();
// Use @JsonTypeInfo and @JsonSubTypes annotations explicitlyFix for Python:
Never use pickle for untrusted data. Use json.loads() which only deserializes JSON primitives — no arbitrary code execution.
5. JSON ReDoS (Regular Expression Denial of Service)
JSON validation using poorly written regular expressions can be exploited to cause catastrophic backtracking:
// UNSAFE — regex can be DoS'd with a crafted input string
const isJson = (str) => /^[],:{}s]*$/.test(str); // simplified vulnerable exampleFix: Use JSON.parse() inside a try/catch — it is implemented in native code (V8) and is not vulnerable to ReDoS:
function isValidJSON(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}Never validate JSON with a custom regex.
6. Sensitive Data Exposure in JSON Responses
A common mistake is accidentally including sensitive fields in API responses:
// UNSAFE — password hash exposed!
app.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // includes passwordHash, salt, secretTokens...
});Fix: Explicitly select or exclude fields:
// SAFE — explicitly pick public fields
app.get("/users/:id", async (req, res) => {
const user = await User.findById(req.params.id)
.select("-passwordHash -salt -resetToken"); // Mongoose: exclude fields
res.json(user);
});
// Or use a serializer / DTO layer
function toPublicUser(user) {
const { passwordHash, salt, ...publicFields } = user.toObject();
return publicFields;
}7. CSRF via JSON
Cross-Site Request Forgery attacks can target JSON APIs if they accept text/plain or application/x-www-form-urlencoded content types (which browsers send in simple cross-origin requests).
Fix: Require Content-Type: application/json and validate it:
app.use((req, res, next) => {
if (req.method !== "GET" && !req.is("application/json")) {
return res.status(415).json({ error: "Content-Type must be application/json" });
}
next();
});8. JSON Size and Depth Limits
Attackers can send deeply nested JSON to exhaust stack depth during parsing, or send enormous JSON payloads to exhaust memory:
// In Express — limit incoming JSON body size
app.use(express.json({
limit: "1mb", // reject bodies over 1 MB
strict: true, // only accept arrays and objects at the root
}));For deeply nested JSON, consider a JSON parser with a depth limit like secure-json-parse:
npm install secure-json-parseconst sjp = require("secure-json-parse");
const data = sjp.parse(rawString); // throws on prototype pollution attemptsSecurity Checklist
- Use parameterized queries — never interpolate JSON values into SQL
- Sanitize MongoDB queries — strip
$operator keys from untrusted input - Disable polymorphic deserialization in Java/Jackson
- Use
JSON.parse()for validation — never a custom regex - Never serialize entire database objects — use explicit field selection
- Require
Content-Type: application/jsonon mutation endpoints - Set JSON body size and depth limits in your HTTP framework
- Validate all incoming JSON against a schema (Zod, Ajv, or JSON Schema)
- Log invalid schema attempts — they often indicate probing attacks