VYPR
Low severity2.9NVD Advisory· Published Feb 11, 2026· Updated Apr 15, 2026

CVE-2025-69873

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.

PackageAffected versionsPatched versions
ajvnpm
>= 7.0.0-alpha.0, < 8.18.08.18.0
ajvnpm
< 6.14.06.14.0

Affected products

1

Patches

1
720a23fa453f

fix(pattern): use configured RegExp engine with $data keyword to mitigate ReDoS attacks (CVE-2025-69873) (#2586)

https://github.com/ajv-validator/ajvEvgenyFeb 14, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.