From 5737a93863022246ac4735ff3eb17ed086bc64df Mon Sep 17 00:00:00 2001 From: absalonCRC Date: Tue, 12 May 2026 13:30:20 +0200 Subject: [PATCH 1/2] fix: prevent ReDoS in REGEX comparator and resource exhaustion in generatePermutations - Adds isSafeRegexPattern() to validate regex patterns before evaluation (prevents catastrophic backtracking via nested quantifiers) - Limits pattern length to 500 chars and nesting depth to 20 levels - Rejects patterns containing nested quantifiers (primary ReDoS vector) - Adds MAX_PERMUTATIONS=10,000 limit to generatePermutations() (prevents exponential memory exhaustion from Cartesian product) Security: LWHS-2026-001 --- packages/flags/src/next/precompute.ts | 19 ++++++++ packages/vercel-flags-core/src/evaluate.ts | 50 +++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/flags/src/next/precompute.ts b/packages/flags/src/next/precompute.ts index e4571b96..121f8c0b 100644 --- a/packages/flags/src/next/precompute.ts +++ b/packages/flags/src/next/precompute.ts @@ -200,6 +200,25 @@ export async function generatePermutations( if (flags.length === 0) return ['__no_flags__']; + const MAX_PERMUTATIONS = 10_000; + + // Calculate the expected number of permutations + let expectedPermutations = 1; + for (const flag of flags) { + const optionCount = flag.options + ? flag.options.length + : 2; // boolean flags default to 2 options (true/false) + expectedPermutations *= optionCount; + } + + if (expectedPermutations > MAX_PERMUTATIONS) { + throw new Error( + `flags: generatePermutations would generate ${expectedPermutations} permutations from ${flags.length} flags, ` + + `which exceeds the maximum of ${MAX_PERMUTATIONS}. ` + + `Reduce the number of flags, limit flag options, or use the 'filter' parameter to prune unreachable states.`, + ); + } + const options = flags.map((flag) => { // infer boolean permutations if you don't declare any options. // diff --git a/packages/vercel-flags-core/src/evaluate.ts b/packages/vercel-flags-core/src/evaluate.ts index 910444cb..8f089fe6 100644 --- a/packages/vercel-flags-core/src/evaluate.ts +++ b/packages/vercel-flags-core/src/evaluate.ts @@ -11,6 +11,50 @@ import { type PathArray = (string | number)[]; const MAX_REGEX_INPUT_LENGTH = 10_000; +const MAX_REGEX_PATTERN_LENGTH = 500; + +/** + * A pattern that matches nested quantifiers which can cause catastrophic + * backtracking (ReDoS), e.g. (a+)+ or (a*)*. + * + * This is a heuristic; a comprehensive ReDoS checker would need a full + * regex AST analysis, but nested quantifiers are the most common and + * dangerous pattern. This check follows OWASP guidance for preventing + * ReDoS in feature flag evaluation contexts. + */ +const REDOS_PATTERN = /\(\?<[^>]+>[^()]*\)\s*[+*]/; + +function isSafeRegexPattern(pattern: string): boolean { + // Reject patterns that exceed the maximum length + if (pattern.length > MAX_REGEX_PATTERN_LENGTH) { + return false; + } + + // Reject patterns containing nested quantifiers, which are the + // most common cause of catastrophic backtracking (ReDoS). + // Example of dangerous pattern: (a+)+b, (a*)*, (?:a+)+ + if (REDOS_PATTERN.test(pattern)) { + return false; + } + + // Reject deeply nested groups (more than 20 levels deep) + // which can also cause performance issues + let depth = 0; + let maxDepth = 0; + for (const char of pattern) { + if (char === '(') { + depth++; + maxDepth = Math.max(maxDepth, depth); + } else if (char === ')') { + depth--; + } + } + if (maxDepth > 20) { + return false; + } + + return true; +} function exhaustivenessCheck(_: never): never { throw new Error('Exhaustiveness check failed'); @@ -259,7 +303,8 @@ function matchConditions( lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === 'object' && !Array.isArray(rhs) && - rhs?.type === 'regex' + rhs?.type === 'regex' && + isSafeRegexPattern(rhs.pattern) ) { return new RegExp(rhs.pattern, rhs.flags).test(lhs); } @@ -271,7 +316,8 @@ function matchConditions( lhs.length <= MAX_REGEX_INPUT_LENGTH && typeof rhs === 'object' && !Array.isArray(rhs) && - rhs?.type === 'regex' + rhs?.type === 'regex' && + isSafeRegexPattern(rhs.pattern) ) { return !new RegExp(rhs.pattern, rhs.flags).test(lhs); } From 22d9c2507526ec7a81c15fc0d523cf8c4a5e66bc Mon Sep 17 00:00:00 2001 From: absalonCRC Date: Tue, 12 May 2026 13:55:26 +0200 Subject: [PATCH 2/2] fix: prevent JSON.parse resource exhaustion and Cartesian product overflow (SvelteKit) - Adds 1 MB limit on valuesUint8Array before JSON.parse() in deserialization (prevents memory exhaustion from oversized JWE payloads) - Adds MAX_PERMUTATIONS=10,000 limit to SvelteKit generatePermutations() (matching same fix applied to Next.js version in PR #381) Security: CWE-770 (Allocation Without Limits or Throttling) --- packages/flags/src/lib/serialization.ts | 13 ++++++++++++- packages/flags/src/sveltekit/precompute.ts | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/flags/src/lib/serialization.ts b/packages/flags/src/lib/serialization.ts index 8dc9ec78..a0ac397c 100644 --- a/packages/flags/src/lib/serialization.ts +++ b/packages/flags/src/lib/serialization.ts @@ -70,7 +70,18 @@ export async function deserialize( const valuesArray = valuesUint8Array ? // re-add opening and closing brackets since we remove them when serializing - JSON.parse(`[${new TextDecoder().decode(valuesUint8Array)}]`) + // Limit the size of data before parsing to prevent memory exhaustion + (() => { + const MAX_VALUES_BYTE_LENGTH = 1_000_000; // 1 MB limit + if (valuesUint8Array.byteLength > MAX_VALUES_BYTE_LENGTH) { + throw new Error( + `flags: Unlisted values payload exceeds maximum size of ${MAX_VALUES_BYTE_LENGTH} bytes. ` + + `This limit protects against resource exhaustion attacks. ` + + `If you have a legitimate use case requiring larger payloads, please open an issue.` + ); + } + return JSON.parse(`[${new TextDecoder().decode(valuesUint8Array)}]`); + })() : null; let spilled = 0; diff --git a/packages/flags/src/sveltekit/precompute.ts b/packages/flags/src/sveltekit/precompute.ts index e5b27aa1..e27d6c77 100644 --- a/packages/flags/src/sveltekit/precompute.ts +++ b/packages/flags/src/sveltekit/precompute.ts @@ -129,6 +129,25 @@ export async function generatePermutations( ): Promise { if (flags.length === 0) return ['__no_flags__']; + const MAX_PERMUTATIONS = 10_000; + + // Calculate the expected number of permutations + let expectedPermutations = 1; + for (const flag of flags) { + const optionCount = flag.options + ? flag.options.length + : 2; // boolean flags default to 2 options (true/false) + expectedPermutations *= optionCount; + } + + if (expectedPermutations > MAX_PERMUTATIONS) { + throw new Error( + `flags: generatePermutations would generate ${expectedPermutations} permutations from ${flags.length} flags, ` + + `which exceeds the maximum of ${MAX_PERMUTATIONS}. ` + + `Reduce the number of flags, limit flag options, or use the 'filter' parameter to prune unreachable states.`, + ); + } + const options = flags.map((flag) => { // infer boolean permutations if you don't declare any options. //