Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to use RE2 (defaults on native regex engine) #1684

Closed
wants to merge 15 commits into from
Closed
6 changes: 6 additions & 0 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const defaultOptions = {
source: false,
process: undefined, // (code: string) => string
optimize: true,
regExp: RegExp
},
}
```
Expand Down Expand Up @@ -361,6 +362,11 @@ type CodeOptions = {
// Code snippet created with `_` tagged template literal that contains all format definitions,
// it can be the code of actual definitions or `require` call:
// _`require("./my-formats")`
regExp: RegExpEngine
// Developers looking for a ReDoS mitigation may wish to use a DFA regex engine,
// such as node-re2. During validation of a schema, code.regExp will be
// used to match strings against regexes. The supplied object must support
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
// the interface: regExp(regex, unicodeFlag).test(string) => boolean
}

type Source = {
Expand Down
18 changes: 15 additions & 3 deletions lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {getJSONTypes} from "./compile/validate/dataType"
import {eachItem} from "./compile/util"

import * as $dataRefSchema from "./refs/data.json"

import DefaultRegExp from "./runtime/regexp"
const META_IGNORE_OPTIONS: (keyof Options)[] = ["removeAdditional", "useDefaults", "coerceTypes"]
const EXT_SCOPE_NAMES = new Set([
"validate",
Expand Down Expand Up @@ -134,16 +134,24 @@ export interface CurrentOptions {
code?: CodeOptions // NEW
}

type RegExpEngine = (pattern: string, u: string) => RegExpLike

export interface RegExpLike {
test: (s: string) => boolean
}

export interface CodeOptions {
es5?: boolean
lines?: boolean
optimize?: boolean | number
formats?: Code // code to require (or construct) map of available formats - for standalone code
source?: boolean
process?: (code: string, schema?: SchemaEnv) => string
regExp?: RegExpEngine & {code: string} // add type
epoberezkin marked this conversation as resolved.
Show resolved Hide resolved
}

interface InstanceCodeOptions extends CodeOptions {
regExp: RegExpEngine & {code: string}
optimize: number
}

Expand Down Expand Up @@ -231,13 +239,15 @@ function requiredOptions(o: Options): RequiredInstanceOptions {
const s = o.strict
const _optz = o.code?.optimize
const optimize = _optz === true || _optz === undefined ? 1 : _optz || 0
const _regExp = o.code?.regExp
const regExp = _regExp ?? DefaultRegExp
return {
strictSchema: o.strictSchema ?? s ?? true,
strictNumbers: o.strictNumbers ?? s ?? true,
strictTypes: o.strictTypes ?? s ?? "log",
strictTuples: o.strictTuples ?? s ?? "log",
strictRequired: o.strictRequired ?? s ?? false,
code: o.code ? {...o.code, optimize} : {optimize},
code: o.code ? {...o.code, optimize, regExp} : {optimize, regExp},
loopRequired: o.loopRequired ?? MAX_EXPRESSION,
loopEnum: o.loopEnum ?? MAX_EXPRESSION,
meta: o.meta ?? true,
Expand Down Expand Up @@ -278,7 +288,9 @@ export default class Ajv {

constructor(opts: Options = {}) {
opts = this.opts = {...opts, ...requiredOptions(opts)}
const {es5, lines} = this.opts.code
const {es5, lines, regExp} = this.opts.code
this.opts.code.regExp = regExp ?? DefaultRegExp

this.scope = new ValueScope({scope: {}, prefixes: EXT_SCOPE_NAMES, es5, lines})
this.logger = getLogger(opts.logger)
const formatOpt = opts.validateFormats
Expand Down
11 changes: 11 additions & 0 deletions lib/runtime/re2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as re2 from "re2"

// interface RegExpLike {
// test: (s: string) => boolean
// }
// type RegExpEngine = (pattern: string, u: string) => RegExpLike

type Re2 = typeof re2 & {code: string}
;(re2 as Re2).code = 'require("ajv/dist/runtime/re2").default'

export default re2 as Re2
10 changes: 10 additions & 0 deletions lib/runtime/regexp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const defaultRegExp = RegExp

//interface RegExpLike {
// test: (s: string) => boolean
//}
//type RegExpEngine = (pattern: string, u: string) => RegExpLike
type RegExpImport = RegExpConstructor & {code: string}
;(defaultRegExp as RegExpImport).code = 'require("ajv/dist/runtime/regexp").default' // TODO: change type

export default defaultRegExp as RegExpImport // TODO: change type
8 changes: 5 additions & 3 deletions lib/vocabularies/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {KeywordCxt} from "../compile/validate"
import {CodeGen, _, and, or, not, nil, strConcat, getProperty, Code, Name} from "../compile/codegen"
import {alwaysValidSchema, Type} from "../compile/util"
import N from "../compile/names"

import {useFunc} from "../compile/util"
export function checkReportMissingProp(cxt: KeywordCxt, prop: string): void {
const {gen, data, it} = cxt
gen.if(noPropertyInData(gen, data, prop, it.opts.ownProperties), () => {
Expand Down Expand Up @@ -92,10 +92,12 @@ export function callValidateCode(

export function usePattern({gen, it: {opts}}: KeywordCxt, pattern: string): Name {
const u = opts.unicodeRegExp ? "u" : ""
const {regExp} = opts.code

return gen.scopeValue("pattern", {
key: pattern,
ref: new RegExp(pattern, u),
code: _`new RegExp(${pattern}, ${u})`,
ref: regExp(pattern, u),
code: _`${useFunc(gen, regExp)}(${pattern}, ${u})`,
})
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"node-fetch": "^3.0.0",
"nyc": "^15.0.0",
"prettier": "^2.3.1",
"re2": "^1.16.0",
"rollup": "^2.44.0",
"rollup-plugin-terser": "^7.0.2",
"ts-node": "^10.0.0",
Expand Down
33 changes: 33 additions & 0 deletions spec/issues/1683_re2_engine.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import getAjvAllInstances from "../ajv_all_instances"
import {withStandalone} from "../ajv_standalone"
import {_} from "../../dist/compile/codegen/code"
import jsonSchemaTest = require("json-schema-test")
import options from "../ajv_options"
import {afterError, afterEach} from "../after_test"
import chai from "../chai"
import re2 from "../../dist/runtime/re2"

const instances = getAjvAllInstances(options, {
$data: true,
formats: {allowedUnknown: true},
strictTypes: false,
strictTuples: false,
})

instances.forEach((ajv) => {
ajv.opts.code.source = true
ajv.opts.code.formats = _`{allowedUnknown: true}`
ajv.opts.code.regExp = re2
})

jsonSchemaTest(withStandalone(instances), {
description:
"Extra keywords schemas tests of " + instances.length + " ajv instances with different options",
suites: {extras: require("./pattern")},
assert: chai.assert,
afterError,
afterEach,
cwd: __dirname,
hideFolder: "extras/",
timeout: 90000,
})
4 changes: 4 additions & 0 deletions spec/issues/pattern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = [
{name: "$data/format", test: require("../extras/$data/format.json")},
{name: "$data/pattern", test: require("../extras/$data/pattern.json")},
]