Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/flags/src/lib/serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions packages/flags/src/next/precompute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down
19 changes: 19 additions & 0 deletions packages/flags/src/sveltekit/precompute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ export async function generatePermutations(
): Promise<string[]> {
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.
//
Expand Down
50 changes: 48 additions & 2 deletions packages/vercel-flags-core/src/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -259,7 +303,8 @@ function matchConditions<T>(
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);
}
Expand All @@ -271,7 +316,8 @@ function matchConditions<T>(
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);
}
Expand Down