豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 720a23f

Browse files
epoberezkinLucas Akira Uehara
andauthored
fix(pattern): use configured RegExp engine with $data keyword to mitigate ReDoS attacks (CVE-2025-69873) (#2586)
* fix(pattern): address CVE-2025-69873 by implementing safeguards against ReDoS attacks in pattern validation * remove console.log * remove Node.js 16 CI build --------- Co-authored-by: Lucas Akira Uehara <80917717@telefonicati.onmicrosoft.com>
1 parent 82735a1 commit 720a23f

File tree

3 files changed

+193
-6
lines changed

3 files changed

+193
-6
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
node-version: [16.x, 18.x, 20.x, 21.x]
15+
node-version: [18.x, 20.x, 21.x]
1616

1717
steps:
1818
- uses: actions/checkout@v4
@@ -23,7 +23,7 @@ jobs:
2323
- run: npm install
2424
- run: git submodule update --init
2525
- name: update website
26-
if: ${{ github.event_name == 'push' && matrix.node-version == '16.x' }}
26+
if: ${{ github.event_name == 'push' && matrix.node-version == '18.x' }}
2727
run: ./scripts/publish-site
2828
env:
2929
GH_TOKEN_PUBLIC: ${{ secrets.GH_TOKEN_PUBLIC }}

lib/vocabularies/validation/pattern.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {CodeKeywordDefinition, ErrorObject, KeywordErrorDefinition} from "../../types"
22
import type {KeywordCxt} from "../../compile/validate"
33
import {usePattern} from "../code"
4+
import {useFunc} from "../../compile/util"
45
import {_, str} from "../../compile/codegen"
56

67
export type PatternError = ErrorObject<"pattern", {pattern: string}, string | {$data: string}>
@@ -17,11 +18,21 @@ const def: CodeKeywordDefinition = {
1718
$data: true,
1819
error,
1920
code(cxt: KeywordCxt) {
20-
const {data, $data, schema, schemaCode, it} = cxt
21-
// TODO regexp should be wrapped in try/catchs
21+
const {gen, data, $data, schema, schemaCode, it} = cxt
2222
const u = it.opts.unicodeRegExp ? "u" : ""
23-
const regExp = $data ? _`(new RegExp(${schemaCode}, ${u}))` : usePattern(cxt, schema)
24-
cxt.fail$data(_`!${regExp}.test(${data})`)
23+
if ($data) {
24+
const {regExp} = it.opts.code
25+
const regExpCode = regExp.code === "new RegExp" ? _`new RegExp` : useFunc(gen, regExp)
26+
const valid = gen.let("valid")
27+
gen.try(
28+
() => gen.assign(valid, _`${regExpCode}(${schemaCode}, ${u}).test(${data})`),
29+
() => gen.assign(valid, false)
30+
)
31+
cxt.fail$data(_`!${valid}`)
32+
} else {
33+
const regExp = usePattern(cxt, schema)
34+
cxt.fail$data(_`!${regExp}.test(${data})`)
35+
}
2536
},
2637
}
2738

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import _Ajv from "../ajv"
2+
import re2 from "../../dist/runtime/re2"
3+
import chai from "../chai"
4+
chai.should()
5+
6+
describe("CVE-2025-69873: ReDoS Attack Scenario", () => {
7+
it("should prevent ReDoS with RE2 engine for $data pattern injection", () => {
8+
const ajv = new _Ajv({$data: true, code: {regExp: re2}})
9+
10+
// Schema that accepts pattern from data
11+
const schema = {
12+
type: "object",
13+
properties: {
14+
pattern: {type: "string"},
15+
value: {type: "string", pattern: {$data: "1/pattern"}},
16+
},
17+
}
18+
19+
const validate = ajv.compile(schema)
20+
21+
// CVE-2025-69873 Attack Payload:
22+
// Pattern: ^(a|a)*$ - catastrophic backtracking regex
23+
// Value: 30 a's + X - forces full exploration of exponential paths
24+
const maliciousPayload = {
25+
pattern: "^(a|a)*$",
26+
value: "a".repeat(30) + "X",
27+
}
28+
29+
const start = Date.now()
30+
const result = validate(maliciousPayload)
31+
const elapsed = Date.now() - start
32+
33+
// Should fail validation (pattern doesn't match)
34+
result.should.equal(false)
35+
36+
// Should complete quickly with RE2 (< 500ms)
37+
// Without RE2, this would hang for 44+ seconds
38+
elapsed.should.be.below(500)
39+
})
40+
41+
it("should handle pattern injection gracefully with default engine", () => {
42+
const ajv = new _Ajv({$data: true})
43+
44+
const schema = {
45+
type: "object",
46+
properties: {
47+
pattern: {type: "string"},
48+
value: {type: "string", pattern: {$data: "1/pattern"}},
49+
},
50+
}
51+
52+
const validate = ajv.compile(schema)
53+
54+
// Attack payload
55+
const maliciousPayload = {
56+
pattern: "^(a|a)*$",
57+
value: "a".repeat(20) + "X", // Reduced size to avoid hanging
58+
}
59+
60+
// Should complete without crashing (might be slow but won't hang forever)
61+
// With try/catch, invalid pattern results in validation failure
62+
const result = validate(maliciousPayload)
63+
result.should.be.a("boolean")
64+
})
65+
66+
it("should handle multiple ReDoS patterns gracefully", () => {
67+
const ajv = new _Ajv({$data: true, code: {regExp: re2}})
68+
69+
const schema = {
70+
type: "object",
71+
properties: {
72+
pattern: {type: "string"},
73+
value: {type: "string", pattern: {$data: "1/pattern"}},
74+
},
75+
}
76+
77+
const validate = ajv.compile(schema)
78+
79+
// Various ReDoS-vulnerable patterns
80+
const redosPatterns = ["^(a+)+$", "^(a|a)*$", "^(a|ab)*$", "(x+x+)+y", "(a*)*b"]
81+
82+
for (const pattern of redosPatterns) {
83+
const start = Date.now()
84+
const result = validate({
85+
pattern,
86+
value: "a".repeat(25) + "X",
87+
})
88+
const elapsed = Date.now() - start
89+
90+
// All should complete quickly with RE2
91+
elapsed.should.be.below(500, `Pattern ${pattern} took too long: ${elapsed}ms`)
92+
result.should.equal(false)
93+
}
94+
})
95+
96+
it("should still validate valid patterns correctly", () => {
97+
const ajv = new _Ajv({$data: true, code: {regExp: re2}})
98+
99+
const schema = {
100+
type: "object",
101+
properties: {
102+
pattern: {type: "string"},
103+
value: {type: "string", pattern: {$data: "1/pattern"}},
104+
},
105+
}
106+
107+
const validate = ajv.compile(schema)
108+
109+
// Valid pattern matching tests
110+
validate({pattern: "^[a-z]+$", value: "abc"}).should.equal(true)
111+
validate({pattern: "^[a-z]+$", value: "ABC"}).should.equal(false)
112+
validate({pattern: "^\\d{3}-\\d{4}$", value: "123-4567"}).should.equal(true)
113+
validate({pattern: "^\\d{3}-\\d{4}$", value: "12-345"}).should.equal(false)
114+
})
115+
116+
it("should fail gracefully on invalid regex syntax in pattern", () => {
117+
const ajv = new _Ajv({$data: true, code: {regExp: re2}})
118+
119+
const schema = {
120+
type: "object",
121+
properties: {
122+
pattern: {type: "string"},
123+
value: {type: "string", pattern: {$data: "1/pattern"}},
124+
},
125+
}
126+
127+
const validate = ajv.compile(schema)
128+
129+
// Invalid regex patterns that RE2 rejects
130+
const invalidPatterns = [
131+
"[invalid", // Unclosed bracket
132+
"(?P<name>...)", // Perl-style named groups not supported
133+
]
134+
135+
for (const pattern of invalidPatterns) {
136+
// RE2 rejects these patterns, resulting in validation failure
137+
const result = validate({
138+
pattern,
139+
value: "test",
140+
})
141+
// Invalid patterns should fail validation
142+
if (!result) {
143+
result.should.equal(false)
144+
}
145+
}
146+
})
147+
148+
it("should process attack payload with safe timing benchmark", () => {
149+
const ajv = new _Ajv({$data: true, code: {regExp: re2}})
150+
151+
const schema = {
152+
type: "object",
153+
properties: {
154+
pattern: {type: "string"},
155+
value: {type: "string", pattern: {$data: "1/pattern"}},
156+
},
157+
}
158+
159+
const validate = ajv.compile(schema)
160+
161+
// Process the exact CVE attack payload
162+
const payload = {
163+
pattern: "^(a|a)*$",
164+
value: "a".repeat(30) + "X",
165+
}
166+
167+
// With RE2: should complete in < 100ms
168+
// Without RE2: would hang for 44+ seconds
169+
const start = Date.now()
170+
const result = validate(payload)
171+
const elapsed = Date.now() - start
172+
173+
result.should.equal(false)
174+
elapsed.should.be.below(500)
175+
})
176+
})

0 commit comments

Comments
 (0)