ajv-validator / ajv-validator/ajv

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

オープン
#2,624 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
主要言語
TypeScript
スター
14.8k
フォーク
1k
PR マージ指標
30日以内にマージされた PR はありません

説明

### 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)

```js
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

```js
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+

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。