From 46108b4eab13eca02d80be292d60967ea72a5fa0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 18:17:27 +0000 Subject: [PATCH] fix(remote-config): resolve ReDoS vulnerability in Custom Signal Regex Evaluation Implemented a robust sandbox around dynamic Regular Expression evaluation using Node.js's native `vm` module to enforce strict execution timeouts. This mitigates catastrophic backtracking (ReDoS) from maliciously crafted regex patterns or payload strings within the Remote Config Condition Evaluator. To avoid severe performance penalties, a single V8 evaluation context is pre-compiled and reused for all subsequent regex evaluations. Added unit test coverage for catastrophic backtracking patterns and invalid regex strings to ensure evaluation safety and event-loop integrity. Co-authored-by: lahirumaramba <55609+lahirumaramba@users.noreply.github.com> --- .../condition-evaluator-internal.ts | 36 +++++++++++++++++-- .../remote-config/condition-evaluator.spec.ts | 2 ++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/remote-config/condition-evaluator-internal.ts b/src/remote-config/condition-evaluator-internal.ts index 73a878109f..dc925be8f8 100644 --- a/src/remote-config/condition-evaluator-internal.ts +++ b/src/remote-config/condition-evaluator-internal.ts @@ -28,7 +28,7 @@ import { CustomSignalOperator, } from './remote-config-api'; import { createHash } from 'crypto'; - +import * as vm from 'vm'; /** * Encapsulates condition evaluation logic to simplify organization and @@ -226,7 +226,7 @@ export class ConditionEvaluator { return compareStrings( targetCustomSignalValues, actualCustomSignalValue, - (target, actual) => new RegExp(target).test(actual), + (target, actual) => safeRegexTest(target, actual), ); // For numeric operators only one target value is allowed. @@ -274,6 +274,38 @@ function compareStrings( return targetValues.some((target) => predicateFn(target, actual)); } +// Pre-compile a single V8 context to avoid the high performance penalty of +// creating a new context for every regex evaluation. +const sharedRegexSandbox = { + pattern: '', + actual: '', + result: false, +}; +const sharedRegexContext = vm.createContext(sharedRegexSandbox); +const sharedRegexScript = new vm.Script(` + try { + result = new RegExp(pattern).test(actual); + } catch (e) { + result = false; + } +`); + +// Compares a regex against an actual string safely with a timeout to mitigate ReDoS. +function safeRegexTest(pattern: string, actual: string): boolean { + try { + sharedRegexSandbox.pattern = pattern; + sharedRegexSandbox.actual = actual; + sharedRegexSandbox.result = false; + + // Timeout is set to 100ms. + sharedRegexScript.runInContext(sharedRegexContext, { timeout: 100 }); + return sharedRegexSandbox.result; + } catch (e) { + // Return false if the regex evaluation times out or throws any other error. + return false; + } +} + // Compares two numbers against each other. // Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target. function compareNumbers( diff --git a/test/unit/remote-config/condition-evaluator.spec.ts b/test/unit/remote-config/condition-evaluator.spec.ts index 764d1b76ef..581dc0b48f 100644 --- a/test/unit/remote-config/condition-evaluator.spec.ts +++ b/test/unit/remote-config/condition-evaluator.spec.ts @@ -1025,6 +1025,8 @@ describe('ConditionEvaluator', () => { const testCases: CustomSignalTestCase[] = [ { targets: ['foo', '^ba.*$'], actual: 'bar', outcome: true }, { targets: [' bar '], actual: 'biz', outcome: false }, + { targets: ['^(a+)+$'], actual: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX', outcome: false }, + { targets: ['[a-z)+'], actual: 'test', outcome: false }, // invalid regex pattern ]; testCases.forEach(runCustomSignalTestCase(CustomSignalOperator.STRING_CONTAINS_REGEX));