ajv-validator / ajv-validator/ajv
uniqueItems/enum/const crash with TypeError when input contains object with non-function toString or valueOf property
- 主要言語
- 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 はまだ評価されていません。