uniqueItems/enum/const crash with TypeError when input contains object with non-function toString or valueOf property
Description
The uniqueItems, enum, and const keywords (which all rely on fast-deep-equal for deep equality) crash with an unhandled TypeError when comparing objects whose toString or valueOf property is set to a non-function value (a string, number, null, etc.). Such objects are perfectly valid JSON input.
Reproducer (ajv 8.20.0, latest on npm)
const Ajv = require("ajv").default || require("ajv");
const ajv = new Ajv();
// 1. uniqueItems crash (data-only, no schema config needed)
const uniq = ajv.compile({ type: "array", uniqueItems: true });
uniq([{}, { toString: "" }]); // TypeError: a.toString is not a function
// 2. enum crash (schema controls trigger)
const en = ajv.compile({ enum: [{}, { valueOf: 0 }] });
en({}); // TypeError: a.valueOf is not a functionOther triggering inputs: {toString: null}, {toString: 0}, {toString: false}, {valueOf: null}, {valueOf: ""}, etc.
Root cause
fast-deep-equal (used via runtime/equal.ts) unconditionally calls a.valueOf() / a.toString() for object comparison without first checking typeof a.valueOf === "function". When the property shadows the inherited Object.prototype.valueOf/toString with a non-function value, the call throws.
Impact
uniqueItems: trueon any user-supplied array: a 22-byte JSON payload[{},{"toString":""}]crashes the validator. Public API endpoints that use ajv to validate inbound arrays are vulnerable to easy DoS unless the host application wraps everyvalidate()call intry/catch.enum/constwith attacker-controlled schema (less common but real, e.g. config-driven schema engines): same crash with the malicious value in the schema array.
The crash propagates as an unhandled TypeError out of the precompiled validator function. ajv's normal error-collection path is bypassed entirely.
Property that fails
fc.assert(fc.property(
fc.array(fc.dictionary(fc.string(), fc.anything())),
(arr) => {
// any well-formed JSON array should not crash uniqueItems validation
const v = ajv.compile({ type: "array", uniqueItems: true });
try { v(arr); return true; } catch (e) {
if (e instanceof TypeError) return false; // unexpected
throw e;
}
}
));
// Shrunk failing input: [{}, {"toString": ""}]Suggested fix
In lib/runtime/equal.ts, wrap the call in a try/catch and fall back to a safe comparator, or fix it upstream in fast-deep-equal by guarding the call:
- if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
- if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
+ const aV = a.valueOf, bV = b.valueOf;
+ if (typeof aV === "function" && aV !== Object.prototype.valueOf) {
+ if (typeof bV !== "function") return false;
+ return aV.call(a) === bV.call(b);
+ }
+ const aT = a.toString, bT = b.toString;
+ if (typeof aT === "function" && aT !== Object.prototype.toString) {
+ if (typeof bT !== "function") return false;
+ return aT.call(a) === bT.call(b);
+ }(The exact upstream patch depends on whether you want to forward the report to fast-deep-equal.)
Environment
- ajv: 8.20.0 (also reproduces in 8.17.1)
- Node: 20+
Source: ajv-validator/ajv