diff --git a/src/utils/__tests__/arrayParser.test.ts b/src/utils/__tests__/arrayParser.test.ts index a19be8d9..27fac5a6 100644 --- a/src/utils/__tests__/arrayParser.test.ts +++ b/src/utils/__tests__/arrayParser.test.ts @@ -80,6 +80,21 @@ describe("parseTextToArray", () => { } }); + it("rejects deeply nested quantifier patterns that bypass simple regex checks", () => { + const deeplyNestedPatterns = ["((a+))+", "((a*)+)", "(((x+))+)+"]; + for (const pattern of deeplyNestedPatterns) { + const result = parseTextToArray("test", { + splitMode: "regex", + delimiter: "*", + regexPattern: pattern, + trimItems: true, + removeEmpty: true, + }); + expect(result.items).toEqual([]); + expect(result.error).toContain("nested quantifiers"); + } + }); + it("allows safe regex patterns", () => { const safePatterns = ["\\d+", "[,;]+", "\\s+", "(a|b)"]; for (const pattern of safePatterns) { diff --git a/src/utils/arrayParser.ts b/src/utils/arrayParser.ts index 8e16ef8c..00b544c2 100644 --- a/src/utils/arrayParser.ts +++ b/src/utils/arrayParser.ts @@ -18,14 +18,39 @@ const MAX_REGEX_INPUT_LENGTH = 100_000; /** * Detect regex patterns prone to catastrophic backtracking (ReDoS). - * Rejects nested quantifiers like (a+)+, (a*)+, (a+)*, etc. + * Rejects nested quantifiers like (a+)+, (a*)+, ((a+))+, etc. + * Uses character-by-character parsing to track nesting depth. */ function isUnsafePattern(pattern: string): boolean { - // Strip /pattern/flags wrapper to inspect the body const slashFormat = pattern.match(/^\/(.+)\/[a-z]*$/i); const body = slashFormat ? slashFormat[1] : pattern; - // Nested quantifiers: a group containing a quantifier, followed by another quantifier - return /\([^)]*[+*]\)[+*{]/.test(body); + + // Track groups: when we see ')' followed by a quantifier, + // check if anything inside that group also had a quantifier. + let depth = 0; + const quantifierAtDepth: boolean[] = []; + + for (let i = 0; i < body.length; i++) { + const ch = body[i]; + if (ch === '\\') { i++; continue; } // skip escaped chars + if (ch === '(') { + depth++; + quantifierAtDepth[depth] = false; + } else if (ch === ')') { + const hadQuantifier = quantifierAtDepth[depth] || false; + depth = Math.max(0, depth - 1); + // Check if this closing paren is followed by a quantifier + const next = body[i + 1]; + if (next === '+' || next === '*' || next === '{') { + if (hadQuantifier) return true; // nested quantifier! + // Mark parent depth as having a quantifier + quantifierAtDepth[depth] = true; + } + } else if ((ch === '+' || ch === '*') && depth > 0) { + quantifierAtDepth[depth] = true; + } + } + return false; } function parseRegexPattern(pattern: string): RegExp {