CVE-2025-69873
Description
ajv (Another JSON Schema Validator) before 8.18.0 is vulnerable to Regular Expression Denial of Service (ReDoS) when the $data option is enabled. The pattern keyword accepts runtime data via JSON Pointer syntax ($data reference), which is passed directly to the JavaScript RegExp() constructor without validation. An attacker can inject a malicious regex pattern (e.g., "^(a|a)*$") combined with crafted input to cause catastrophic backtracking. A 31-character payload causes approximately 44 seconds of CPU blocking, with each additional character doubling execution time. This enables complete denial of service with a single HTTP request against any API using ajv with $data: true for dynamic schema validation. This issue is also fixed in version 6.14.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ajvnpm | >= 7.0.0-alpha.0, < 8.18.0 | 8.18.0 |
ajvnpm | < 6.14.0 | 6.14.0 |
Affected products
1Patches
1720a23fa453ffix(pattern): use configured RegExp engine with $data keyword to mitigate ReDoS attacks (CVE-2025-69873) (#2586)
3 files changed · +193 −6
.github/workflows/build.yml+2 −2 modified@@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.x, 18.x, 20.x, 21.x] + node-version: [18.x, 20.x, 21.x] steps: - uses: actions/checkout@v4 @@ -23,7 +23,7 @@ jobs: - run: npm install - run: git submodule update --init - name: update website - if: ${{ github.event_name == 'push' && matrix.node-version == '16.x' }} + if: ${{ github.event_name == 'push' && matrix.node-version == '18.x' }} run: ./scripts/publish-site env: GH_TOKEN_PUBLIC: ${{ secrets.GH_TOKEN_PUBLIC }}
lib/vocabularies/validation/pattern.ts+15 −4 modified@@ -1,6 +1,7 @@ import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types" import type {KeywordCxt} from "../../compile/validate" import {usePattern} from "../code" +import {useFunc} from "../../compile/util" import {_, str} from "../../compile/codegen" export type PatternError = ErrorObject<"pattern", {pattern: string}, string | {$data: string}> @@ -17,11 +18,21 @@ const def: CodeKeywordDefinition = { $data: true, error, code(cxt: KeywordCxt) { - const {data, $data, schema, schemaCode, it} = cxt - // TODO regexp should be wrapped in try/catchs + const {gen, data, $data, schema, schemaCode, it} = cxt const u = it.opts.unicodeRegExp ? "u" : "" - const regExp = $data ? _`(new RegExp(${schemaCode}, ${u}))` : usePattern(cxt, schema) - cxt.fail$data(_`!${regExp}.test(${data})`) + if ($data) { + const {regExp} = it.opts.code + const regExpCode = regExp.code === "new RegExp" ? _`new RegExp` : useFunc(gen, regExp) + const valid = gen.let("valid") + gen.try( + () => gen.assign(valid, _`${regExpCode}(${schemaCode}, ${u}).test(${data})`), + () => gen.assign(valid, false) + ) + cxt.fail$data(_`!${valid}`) + } else { + const regExp = usePattern(cxt, schema) + cxt.fail$data(_`!${regExp}.test(${data})`) + } }, }
spec/issues/cve_2025_69873_redos_attack.spec.ts+176 −0 added@@ -0,0 +1,176 @@ +import _Ajv from "../ajv" +import re2 from "../../dist/runtime/re2" +import chai from "../chai" +chai.should() + +describe("CVE-2025-69873: ReDoS Attack Scenario", () => { + it("should prevent ReDoS with RE2 engine for $data pattern injection", () => { + const ajv = new _Ajv({$data: true, code: {regExp: re2}}) + + // Schema that accepts pattern from data + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // CVE-2025-69873 Attack Payload: + // Pattern: ^(a|a)*$ - catastrophic backtracking regex + // Value: 30 a's + X - forces full exploration of exponential paths + const maliciousPayload = { + pattern: "^(a|a)*$", + value: "a".repeat(30) + "X", + } + + const start = Date.now() + const result = validate(maliciousPayload) + const elapsed = Date.now() - start + + // Should fail validation (pattern doesn't match) + result.should.equal(false) + + // Should complete quickly with RE2 (< 500ms) + // Without RE2, this would hang for 44+ seconds + elapsed.should.be.below(500) + }) + + it("should handle pattern injection gracefully with default engine", () => { + const ajv = new _Ajv({$data: true}) + + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // Attack payload + const maliciousPayload = { + pattern: "^(a|a)*$", + value: "a".repeat(20) + "X", // Reduced size to avoid hanging + } + + // Should complete without crashing (might be slow but won't hang forever) + // With try/catch, invalid pattern results in validation failure + const result = validate(maliciousPayload) + result.should.be.a("boolean") + }) + + it("should handle multiple ReDoS patterns gracefully", () => { + const ajv = new _Ajv({$data: true, code: {regExp: re2}}) + + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // Various ReDoS-vulnerable patterns + const redosPatterns = ["^(a+)+$", "^(a|a)*$", "^(a|ab)*$", "(x+x+)+y", "(a*)*b"] + + for (const pattern of redosPatterns) { + const start = Date.now() + const result = validate({ + pattern, + value: "a".repeat(25) + "X", + }) + const elapsed = Date.now() - start + + // All should complete quickly with RE2 + elapsed.should.be.below(500, `Pattern ${pattern} took too long: ${elapsed}ms`) + result.should.equal(false) + } + }) + + it("should still validate valid patterns correctly", () => { + const ajv = new _Ajv({$data: true, code: {regExp: re2}}) + + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // Valid pattern matching tests + validate({pattern: "^[a-z]+$", value: "abc"}).should.equal(true) + validate({pattern: "^[a-z]+$", value: "ABC"}).should.equal(false) + validate({pattern: "^\\d{3}-\\d{4}$", value: "123-4567"}).should.equal(true) + validate({pattern: "^\\d{3}-\\d{4}$", value: "12-345"}).should.equal(false) + }) + + it("should fail gracefully on invalid regex syntax in pattern", () => { + const ajv = new _Ajv({$data: true, code: {regExp: re2}}) + + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // Invalid regex patterns that RE2 rejects + const invalidPatterns = [ + "[invalid", // Unclosed bracket + "(?P<name>...)", // Perl-style named groups not supported + ] + + for (const pattern of invalidPatterns) { + // RE2 rejects these patterns, resulting in validation failure + const result = validate({ + pattern, + value: "test", + }) + // Invalid patterns should fail validation + if (!result) { + result.should.equal(false) + } + } + }) + + it("should process attack payload with safe timing benchmark", () => { + const ajv = new _Ajv({$data: true, code: {regExp: re2}}) + + const schema = { + type: "object", + properties: { + pattern: {type: "string"}, + value: {type: "string", pattern: {$data: "1/pattern"}}, + }, + } + + const validate = ajv.compile(schema) + + // Process the exact CVE attack payload + const payload = { + pattern: "^(a|a)*$", + value: "a".repeat(30) + "X", + } + + // With RE2: should complete in < 100ms + // Without RE2: would hang for 44+ seconds + const start = Date.now() + const result = validate(payload) + const elapsed = Date.now() - start + + result.should.equal(false) + elapsed.should.be.below(500) + }) +})
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
10- github.com/advisories/GHSA-2g4f-4pwh-qvx6nvdADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-69873ghsaADVISORY
- github.com/EthanKim88/ethan-cve-disclosures/blob/main/CVE-2025-69873-ajv-ReDoS.mdnvdWEB
- github.com/ajv-validator/ajv/commit/720a23fa453ffae8340e92c9b0fe886c54cfe0d5ghsaWEB
- github.com/ajv-validator/ajv/pull/2586ghsaWEB
- github.com/ajv-validator/ajv/pull/2588nvdWEB
- github.com/ajv-validator/ajv/pull/2590nvdWEB
- github.com/ajv-validator/ajv/releases/tag/v6.14.0nvdWEB
- github.com/ajv-validator/ajv/releases/tag/v8.18.0ghsaWEB
- github.com/github/advisory-database/pull/6991nvdWEB
News mentions
0No linked articles in our index yet.