JSON Security Best Practices

General ~4 min read
JSONSecurityValidationBest Practices

Protect your applications from JSON-related vulnerabilities. Learn about injection attacks, prototype pollution, DoS prevention, input validation, and secure deserialization patterns.

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

JSON API Comparison Across Languages

Language Library Serialize Deserialize Pretty Print Config
Python json(内置) json.dumps(obj) json.loads(str) json.dumps(obj, indent=2) ensure_ascii, default, cls
JavaScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Java Jackson / Gson mapper.writeValueAsString(obj) mapper.readValue(str, Class.class) mapper.writerWithDefaultPrettyPrinter() 注解、Module、Feature
Go encoding/json(内置) json.Marshal(obj) json.Unmarshal(data, &obj) json.MarshalIndent(obj, "", " ") struct tag
C# System.Text.Json(内置) JsonSerializer.Serialize(obj) JsonSerializer.Deserialize<T>(str) WriteIndented = true JsonSerializerOptions
TypeScript JSON(内置) JSON.stringify(obj) JSON.parse(str) JSON.stringify(obj, null, 2) replacer, space
Rust serde_json serde_json::to_string(&obj) serde_json::from_str::<T>(str) serde_json::to_string_pretty(&obj) serde attributes, custom (de)serializers

Frequently Asked Questions (FAQ)

How to handle datetime types in JSON serialization?

Most language JSON libraries do not support datetime types by default. The common approach is to serialize as ISO 8601 format strings (e.g. "2026-07-15T10:30:00Z") or Unix timestamps, then convert back to datetime objects during deserialization.

How to ignore null values or empty fields in JSON serialization?

Different languages have different implementations: Python can filter None values in custom encoders; JavaScript can use replacer functions; Java Jackson uses @JsonInclude annotations; Go uses omitempty struct tags; C# sets DefaultIgnoreCondition.

How to handle circular references in JSON serialization?

Circular references are a common pitfall. Standard JSON libraries usually throw exceptions or cause stack overflow. Solutions include using @JsonIdentityInfo (Java Jackson), implementing custom serializers, or converting to DTOs before serialization.

Why are my private fields not being serialized?

JSON serialization libraries typically only serialize public fields or properties with getters. This is a security consideration. You can use annotations like @JsonProperty (Java Jackson) or [JsonInclude] (C#) to include non-public members.

What are the performance optimization tips for JSON serialization?

1) Reuse serializer instances; 2) Use streaming APIs for large data; 3) Use compile-time generation (C# Source Generator, Go easyjson); 4) Avoid excessive nesting; 5) Use database pagination for large JSON.

Recommended Tools

These tools can help you handle JSON data more efficiently in General development:

Related Articles