#2624·ajv

uniqueItems/enum/const crash with TypeError when input contains object with non-function toString or valueOf property

Author: zhangjiashuo-csCreated May 24, 2026Updated Jun 18, 2026

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)

javascript
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 function

Other 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: true on 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 every validate() call in try/catch.
  • enum / const with 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

javascript
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:

diff
- 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+