1+import { checkSync } from "recheck";
2+import { expect, test } from "vitest";
3+import * as z from "zod/v4";
4+
5+const { regexes } = z.core;
6+
7+/** Every pattern `core/regexes.ts` ships, with the parameterized ones materialized across their argument space. */
8+function allPatterns(): [string, RegExp][] {
9+ const patterns: [string, RegExp][] = [];
10+ for (const [name, value] of Object.entries(regexes)) {
11+ if (value instanceof RegExp) patterns.push([name, value]);
12+ }
13+
14+ patterns.push(
15+ ["emoji()", regexes.emoji()],
16+ ["mac()", regexes.mac()],
17+ ["mac('-')", regexes.mac("-")],
18+ ["uuid()", regexes.uuid()],
19+ ["string()", regexes.string()],
20+ ["string({min,max})", regexes.string({ minimum: 1, maximum: 10 })]
21+ );
22+ for (const version of [1, 2, 3, 4, 5, 6, 7, 8]) patterns.push([`uuid(${version})`, regexes.uuid(version)]);
23+ for (const precision of [null, -1, 0, 3, 6]) {
24+ patterns.push([`time(${precision})`, regexes.time({ precision })]);
25+ for (const local of [false, true]) {
26+ for (const offset of [false, true]) {
27+ patterns.push([`datetime(${precision},${local},${offset})`, regexes.datetime({ precision, local, offset })]);
28+ }
29+ }
30+ }
31+ return patterns;
32+}
33+
34+test("no built-in pattern is ReDoS-vulnerable", () => {
35+ const patterns = allPatterns();
36+ // Guards against the reflection above silently going empty if the module layout changes.
37+ expect(patterns.length).toBeGreaterThan(80);
38+
39+ const vulnerable: string[] = [];
40+ for (const [name, pattern] of patterns) {
41+ // Flags are load-bearing. Without "u" the checker reads `\p{...}` as a literal `p{...}`
42+ // and reports an exponential pattern as safe.
43+ const result = checkSync(pattern.source, pattern.flags);
44+ if (result.status !== "safe") vulnerable.push(`${name} (${result.status}): ${pattern.source}`);
45+ }
46+ expect(vulnerable).toEqual([]);
47+}, 60000);
48+
49+test("emoji rejects a backtracking payload in linear time", () => {
50+ // U+1F9B0-U+1F9B3 are the only code points in both \p{Extended_Pictographic} and
51+ // \p{Emoji_Component}. A failing match over them used to backtrack exponentially:
52+ // 26 of them took ~1.9s, and each additional character roughly doubled it.
53+ const start = performance.now();
54+ expect(z.emoji().safeParse(`${"π¦°".repeat(26)} `).success).toBe(false);
55+ expect(performance.now() - start).toBeLessThan(100);
56+});