Resource exhaustion with Ajv allErrors:true

Resource exhaustion from collecting all Ajv validation errors

Description

Ajv's allErrors:true option collects all available validation errors. Generating many error objects and strings and continuing to evaluate schema constraints can increase CPU and memory use. An attacker may submit data that violates many rules, or large and deeply nested JSON, to exhaust resources and cause denial of service (DoS).

Potential impact

  • Excessive error collection and computation may delay responses or crash a process.
  • Thousands of error objects or messages may exhaust heap memory and cause restarts.
  • Continued validation may increase request latency and reduce overall throughput.
  • Large error logs may increase monitoring costs and obscure useful information.

Remediation

  • Avoid allErrors:true in production. Keep the default false behavior, which stops collecting further errors.
  • Enable it through an environment variable only when debugging in development, for example with DEBUG_VALIDATE=1.
  • Limit request body size and connection duration. A request timeout cannot interrupt synchronous validation CPU work.
  • Use rate limiting and circuit breakers to control excessive traffic.
  • Use only trusted schemas and avoid excessive combinations of constraints such as oneOf and anyOf.
  • Bound input structures with limits such as maxItems and maxProperties.

Examples

Before

javascript
import express from "express";
import Ajv from "ajv";

const app = express();
app.use(express.json({ limit: "1mb" }));

// BAD: use allErrors:true in production (collect all errors)
const ajv = new Ajv({ allErrors: true });

const userSchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    name: { type: "string" },
    tags: { type: "array", items: { type: "string" } },
  },
  required: ["name", "tags"],
};

const validateUser = ajv.compile(userSchema);

app.post("/users", (req, res) => {
  const valid = validateUser(req.body);
  if (!valid) {
    // Generating and returning many errors consumes CPU and memory
    return res.status(400).json({ errors: validateUser.errors });
  }
  res.send("ok");
});

app.listen(3000);

After

javascript
import express from "express";
import Ajv from "ajv";

const app = express();
// Set a request size limit and timeout
app.use(express.json({ limit: "512kb" }));
app.use((req, res, next) => {
  req.setTimeout(5000);
  next();
});

// GOOD: default fail-fast behavior; enable allErrors only for development
const enableAllErrors = process.env.NODE_ENV === "development" && process.env.DEBUG_VALIDATE === "1";
const ajv = new Ajv({ allErrors: enableAllErrors });

const userSchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    name: { type: "string" },
    tags: {
      type: "array",
      maxItems: 50,
      items: { type: "string", maxLength: 64 },
    },
  },
  required: ["name", "tags"],
};

const validateUser = ajv.compile(userSchema);

app.post("/users", (req, res) => {
  const valid = validateUser(req.body);
  if (!valid) {
    // Return a summary instead of exposing many error objects
    return res.status(400).json({ message: "Invalid payload" });
  }
  res.send("ok");
});

app.listen(3000);

Explanation:

  • Before: allErrors:true continues checking constraints and collecting errors after validation failures. Inputs that violate many constraints may generate enough errors and computation to cause resource exhaustion and DoS.
  • After: allErrors is enabled only when the development environment and debug flag are both set. Otherwise, the default reduces further error collection. Body limits, maxItems, and maxLength constrain the work. req.setTimeout sets a connection timeout; it does not limit synchronous validation execution time.

References