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:truein production. Keep the defaultfalsebehavior, 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
oneOfandanyOf. - Bound input structures with limits such as
maxItemsandmaxProperties.
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:truecontinues 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:
allErrorsis enabled only when the development environment and debug flag are both set. Otherwise, the default reduces further error collection. Body limits,maxItems, andmaxLengthconstrain the work.req.setTimeoutsets a connection timeout; it does not limit synchronous validation execution time.