From 7595145cbb5b582f46da5afab5135b56337dd35c Mon Sep 17 00:00:00 2001 From: Lucas Akira Uehara <80917717@telefonicati.onmicrosoft.com> Date: Fri, 13 Feb 2026 17:58:35 -0300 Subject: [PATCH 1/3] fix(pattern): address CVE-2025-69873 by implementing safeguards against ReDoS attacks in pattern validation --- lib/vocabularies/validation/pattern.ts | 18 +- .../cve_2025_69873_redos_attack.spec.ts | 183 ++++++++++++++++++ 2 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 spec/issues/cve_2025_69873_redos_attack.spec.ts diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 7b27b7d3c..1eccb61c7 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -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}> @@ -18,10 +19,21 @@ const def: CodeKeywordDefinition = { error, code(cxt: KeywordCxt) { const {data, $data, schema, schemaCode, it} = cxt - // TODO regexp should be wrapped in try/catchs 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 {gen} = cxt + 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})`), + (_e) => gen.assign(valid, false) + ) + cxt.fail$data(_`!${valid}`) + } else { + const regExp = usePattern(cxt, schema) + cxt.fail$data(_`!${regExp}.test(${data})`) + } }, } diff --git a/spec/issues/cve_2025_69873_redos_attack.spec.ts b/spec/issues/cve_2025_69873_redos_attack.spec.ts new file mode 100644 index 000000000..71b522c88 --- /dev/null +++ b/spec/issues/cve_2025_69873_redos_attack.spec.ts @@ -0,0 +1,183 @@ +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...)", // 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) + console.log(`\n ✓ CVE-2025-69873 attack payload processed in ${elapsed}ms (safe: < 500ms)`) + elapsed.should.be.below(500) + }) +}) From a7937459340ae0c0bc586ae5a67f1ba3f7519fb8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 14 Feb 2026 00:05:09 +0000 Subject: [PATCH 2/3] remove console.log --- lib/vocabularies/validation/pattern.ts | 5 ++--- spec/issues/cve_2025_69873_redos_attack.spec.ts | 11 ++--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 1eccb61c7..bb0203b88 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -18,16 +18,15 @@ const def: CodeKeywordDefinition = { $data: true, error, code(cxt: KeywordCxt) { - const {data, $data, schema, schemaCode, it} = cxt + const {gen, data, $data, schema, schemaCode, it} = cxt const u = it.opts.unicodeRegExp ? "u" : "" if ($data) { - const {gen} = cxt 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})`), - (_e) => gen.assign(valid, false) + () => gen.assign(valid, false) ) cxt.fail$data(_`!${valid}`) } else { diff --git a/spec/issues/cve_2025_69873_redos_attack.spec.ts b/spec/issues/cve_2025_69873_redos_attack.spec.ts index 71b522c88..862dc3255 100644 --- a/spec/issues/cve_2025_69873_redos_attack.spec.ts +++ b/spec/issues/cve_2025_69873_redos_attack.spec.ts @@ -77,13 +77,7 @@ describe("CVE-2025-69873: ReDoS Attack Scenario", () => { const validate = ajv.compile(schema) // Various ReDoS-vulnerable patterns - const redosPatterns = [ - "^(a+)+$", - "^(a|a)*$", - "^(a|ab)*$", - "(x+x+)+y", - "(a*)*b", - ] + const redosPatterns = ["^(a+)+$", "^(a|a)*$", "^(a|ab)*$", "(x+x+)+y", "(a*)*b"] for (const pattern of redosPatterns) { const start = Date.now() @@ -134,7 +128,7 @@ describe("CVE-2025-69873: ReDoS Attack Scenario", () => { // Invalid regex patterns that RE2 rejects const invalidPatterns = [ - "[invalid", // Unclosed bracket + "[invalid", // Unclosed bracket "(?P...)", // Perl-style named groups not supported ] @@ -177,7 +171,6 @@ describe("CVE-2025-69873: ReDoS Attack Scenario", () => { const elapsed = Date.now() - start result.should.equal(false) - console.log(`\n ✓ CVE-2025-69873 attack payload processed in ${elapsed}ms (safe: < 500ms)`) elapsed.should.be.below(500) }) }) From 99b83bc650046cdeb1dc6a8ce6d5d9727641d4a7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 14 Feb 2026 00:12:53 +0000 Subject: [PATCH 3/3] remove Node.js 16 CI build --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d374fbd9e..61d7cedb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }}