Skip to content

Commit b552ed6

Browse files
add regExp option to address $data exploit via a regular expression (CVE-2025-69873) (#2590)
* add regExp option to address $data exploit via a regular expression * fix * cleanup * move back --------- Co-authored-by: Evgeny @ SimpleX Chat <[email protected]>
1 parent 72f2286 commit b552ed6

File tree

8 files changed

+928
-11
lines changed

8 files changed

+928
-11
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1215,7 +1215,8 @@ Defaults:
12151215
sourceCode: false,
12161216
processCode: undefined, // function (str: string, schema: object): string {}
12171217
cache: new Cache,
1218-
serialize: undefined
1218+
serialize: undefined,
1219+
regExp: undefined // custom RegExp engine
12191220
}
12201221
```
12211222

@@ -1329,6 +1330,11 @@ Defaults:
13291330
- `transpile` that transpiled asynchronous validation function. You can still use `transpile` option with [ajv-async](https://github.com/ajv-validator/ajv-async) package. See [Asynchronous validation](#asynchronous-validation) for more information.
13301331
- _cache_: an optional instance of cache to store compiled schemas using stable-stringified schema as a key. For example, set-associative cache [sacjs](https://github.com/epoberezkin/sacjs) can be used. If not passed then a simple hash is used which is good enough for the common use case (a limited number of statically defined schemas). Cache should have methods `put(key, value)`, `get(key)`, `del(key)` and `clear()`.
13311332
- _serialize_: an optional function to serialize schema to cache key. Pass `false` to use schema itself as a key (e.g., if WeakMap used as a cache). By default [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used.
1333+
- _regExp_: an optional function to create RegExp objects. This allows using a custom RegExp engine (e.g., [RE2](https://github.com/uhop/node-re2)) to mitigate ReDoS attacks. The function must have the signature `(pattern: string) => RegExpLike` where `RegExpLike` is an object with a `test(string) => boolean` method. Example with RE2:
1334+
```javascript
1335+
var ajv = new Ajv({regExp: require('re2')});
1336+
```
1337+
By default (`undefined`), native `RegExp` constructor is used.
13321338

13331339

13341340
## Validation errors

lib/ajv.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ declare namespace ajv {
203203
logger?: CustomLogger | false;
204204
nullable?: boolean;
205205
serialize?: ((schema: object | boolean) => any) | false;
206+
regExp?: (pattern: string) => RegExpLike;
207+
}
208+
209+
interface RegExpLike {
210+
test: (s: string) => boolean;
206211
}
207212

208213
type FormatValidator = string | RegExp | ((data: string) => boolean | PromiseLike<any>);

lib/compile/index.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ function compile(schema, root, localRefs, baseId) {
4242
, defaultsHash = {}
4343
, customRules = [];
4444

45+
function patternCode(i, patterns) {
46+
var regExpCode = opts.regExp ? 'regExp' : 'new RegExp';
47+
return 'var pattern' + i + ' = ' + regExpCode + '(' + util.toQuotedString(patterns[i]) + ');';
48+
}
49+
4550
root = root || { schema: schema, refVal: refVal, refs: refs };
4651

4752
var c = checkCompiling.call(this, schema, root, baseId);
@@ -128,6 +133,7 @@ function compile(schema, root, localRefs, baseId) {
128133
'equal',
129134
'ucs2length',
130135
'ValidationError',
136+
'regExp',
131137
sourceCode
132138
);
133139

@@ -141,7 +147,8 @@ function compile(schema, root, localRefs, baseId) {
141147
customRules,
142148
equal,
143149
ucs2length,
144-
ValidationError
150+
ValidationError,
151+
opts.regExp
145152
);
146153

147154
refVal[0] = validate;
@@ -358,11 +365,6 @@ function compIndex(schema, root, baseId) {
358365
}
359366

360367

361-
function patternCode(i, patterns) {
362-
return 'var pattern' + i + ' = new RegExp(' + util.toQuotedString(patterns[i]) + ');';
363-
}
364-
365-
366368
function defaultCode(i) {
367369
return 'var default' + i + ' = defaults[' + i + '];';
368370
}

lib/dot/pattern.jst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,22 @@
44
{{# def.$data }}
55

66
{{
7-
var $regexp = $isData
8-
? '(new RegExp(' + $schemaValue + '))'
9-
: it.usePattern($schema);
7+
var $regExpCode = it.opts.regExp ? 'regExp' : 'new RegExp';
108
}}
119

12-
if ({{# def.$dataNotType:'string' }} !{{=$regexp}}.test({{=$data}}) ) {
10+
{{? $isData }}
11+
var {{=$valid}} = true;
12+
try {
13+
{{=$valid}} = {{=$regExpCode}}({{=$schemaValue}}).test({{=$data}});
14+
} catch(e) {
15+
{{=$valid}} = false;
16+
}
17+
if ({{# def.$dataNotType:'string' }} !{{=$valid}}) {
18+
{{??}}
19+
{{
20+
var $regexp = it.usePattern($schema);
21+
}}
22+
if ({{# def.$dataNotType:'string' }} !{{=$regexp}}.test({{=$data}}) ) {
23+
{{?}}
1324
{{# def.error:'pattern' }}
1425
} {{? $breakOnError }} else { {{?}}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"mocha": "^8.0.1",
9191
"nyc": "^15.0.0",
9292
"pre-commit": "^1.1.1",
93+
"re2": "^1.21.4",
9394
"require-globify": "^1.3.0",
9495
"typescript": "^3.9.5",
9596
"uglify-js": "^3.6.9",

0 commit comments

Comments
 (0)