You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The dotCMS Experiments SDK (core-web/libs/sdk/experiments) evaluates server-assembled regular expressions against the current URL on every page view. The evaluation happens in core-web/libs/sdk/experiments/src/lib/shared/parser/parser.ts in the verifyRegex(regexToCheck, href) function and is called from:
parser.ts:35 — parseDataForAnalytics (tracker hot path; runs on every page view when experiments are active).
The regex originates server-side in dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentUrlPatternCalculator.java and is assembled from each Vanity URL's own URI pattern — admins can author arbitrary regex there. It ships to the browser as the regexs.isExperimentPage / regexs.isTargetPage fields of the experiments response.
verifyRegex performs new RegExp(regexToCheck).test(sanitizedHref) with no timeout and no quarantine cache. The surrounding try / catch catches syntactic errors only (invalid regex syntax), not catastrophic backtracking. A malicious or accidentally-pathological Vanity URL pattern (classic example: (a+)+b against an input like aaaaaaaaaaaaaaaaaaaaaaaaaaaaa!) can therefore freeze visitors' browser tabs indefinitely — a client-side DoS whose blast radius is every site visitor for as long as the experiment is active.
The server already has this protection via com.dotcms.regex.MatcherTimeoutFactory (used in VanityUrlAPIImpl.resolveVanityUrl): a TimeLimitedCharSequence wrapper throws RegExpTimeoutException after ~2s and offending patterns are quarantined for 30s. There is no equivalent on the client.
Risk profile: low likelihood in practice (requires an admin to author a bad pattern), but the blast radius is wide (every visitor on the affected site) and the cost is asymmetric (the author burns our visitors' CPU, not the server's).
Surfaced during review of: PR #35360 / related to issue #34747.
Design options to evaluate during grooming
Worker + timeout + quarantine cache — mirror the server-side MatcherTimeoutFactory on the client. Main-thread setTimeout cannot interrupt a running regex, so matching must happen in a Worker that can be .terminate()'d after a timeout. Requires making verifyRegex async, packaging an inline Worker for portability, and threading the API change through parser.ts, dot-experiments.ts, hooks/useExperimentVariant.ts, hooks/useExperiments.ts, and standalone.ts. Strongest protection; highest cost (~1–2 days of engineering + tests).
Static regex linting on fetch — run safe-regex (or equivalent) when the SDK receives experiments from the server and drop/skip patterns flagged as vulnerable. No API change, no runtime overhead per page-view. Lightweight but detection is imperfect (false negatives and false positives).
re2-wasm / re2js as a drop-in RegExp replacement — linear-time matcher by construction; ReDoS impossible. No timeout needed. Trade-off: ~500 KB WASM payload added to the SDK; no support for JS-specific features (backreferences, lookaround) — acceptable for URL matching.
URL-length cap before matching — reject inputs over N characters as a crude ~5-line mitigation. Catches the most common amplification vector (very long input × greedy pattern) only.
Any combination of 2 + 4 as defense-in-depth is also worth considering before committing to the larger option 1 or 3.
Server-side companion for reference: dotCMS/src/main/java/com/dotcms/regex/MatcherTimeoutFactory.java.
Acceptance Criteria
The SDK, when matching against a known-pathological regex/input pair (e.g. (a+)+b vs. a long aaaa…! string), does not freeze the browser tab. Matching either succeeds, is bounded by a configurable timeout, or is rejected before reaching RegExp.
A protection strategy is selected and documented (pick from options 1–4 or a combination).
If a runtime timeout is used: patterns that time out are quarantined (cached as "no match") for a configurable window so repeat invocations of the same pattern in the same session short-circuit without running the regex again.
Any public SDK API changes (e.g. verifyRegex becoming async) are documented in the SDK changelog, and consumers (useExperimentVariant, useExperiments, standalone entry point) continue to function.
Unit tests cover: (a) happy-path match returns correctly, (b) a pathological pattern is bounded (does not hang the test), (c) subsequent calls with the same bad pattern short-circuit (if a quarantine cache is implemented).
Existing SDK behavior for well-formed regex is unchanged (no regressions in parseDataForAnalytics, getVariantFromHref, getVariantAsQueryParam).
Priority
Medium
Additional Context
Parent issue: [DEFECT] Experiments not firing when using VanityURLs #34747 — adds vanity URLs (with admin-controlled regex) into the experiment URL-matching pipeline; resolves the /cmsHomePage scenario but does not add client-side ReDoS protection.
Description
The dotCMS Experiments SDK (
core-web/libs/sdk/experiments) evaluates server-assembled regular expressions against the current URL on every page view. The evaluation happens incore-web/libs/sdk/experiments/src/lib/shared/parser/parser.tsin theverifyRegex(regexToCheck, href)function and is called from:parser.ts:35—parseDataForAnalytics(tracker hot path; runs on every page view when experiments are active).core-web/libs/sdk/experiments/src/lib/dot-experiments.ts:325—getVariantFromHref.core-web/libs/sdk/experiments/src/lib/dot-experiments.ts:344—getVariantAsQueryParam.The regex originates server-side in
dotCMS/src/main/java/com/dotcms/experiments/business/ExperimentUrlPatternCalculator.javaand is assembled from each Vanity URL's own URI pattern — admins can author arbitrary regex there. It ships to the browser as theregexs.isExperimentPage/regexs.isTargetPagefields of the experiments response.verifyRegexperformsnew RegExp(regexToCheck).test(sanitizedHref)with no timeout and no quarantine cache. The surroundingtry / catchcatches syntactic errors only (invalid regex syntax), not catastrophic backtracking. A malicious or accidentally-pathological Vanity URL pattern (classic example:(a+)+bagainst an input likeaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!) can therefore freeze visitors' browser tabs indefinitely — a client-side DoS whose blast radius is every site visitor for as long as the experiment is active.The server already has this protection via
com.dotcms.regex.MatcherTimeoutFactory(used inVanityUrlAPIImpl.resolveVanityUrl): aTimeLimitedCharSequencewrapper throwsRegExpTimeoutExceptionafter ~2s and offending patterns are quarantined for 30s. There is no equivalent on the client.Risk profile: low likelihood in practice (requires an admin to author a bad pattern), but the blast radius is wide (every visitor on the affected site) and the cost is asymmetric (the author burns our visitors' CPU, not the server's).
Surfaced during review of: PR #35360 / related to issue #34747.
Design options to evaluate during grooming
MatcherTimeoutFactoryon the client. Main-threadsetTimeoutcannot interrupt a running regex, so matching must happen in a Worker that can be.terminate()'d after a timeout. Requires makingverifyRegexasync, packaging an inline Worker for portability, and threading the API change throughparser.ts,dot-experiments.ts,hooks/useExperimentVariant.ts,hooks/useExperiments.ts, andstandalone.ts. Strongest protection; highest cost (~1–2 days of engineering + tests).safe-regex(or equivalent) when the SDK receives experiments from the server and drop/skip patterns flagged as vulnerable. No API change, no runtime overhead per page-view. Lightweight but detection is imperfect (false negatives and false positives).re2-wasm/re2jsas a drop-inRegExpreplacement — linear-time matcher by construction; ReDoS impossible. No timeout needed. Trade-off: ~500 KB WASM payload added to the SDK; no support for JS-specific features (backreferences, lookaround) — acceptable for URL matching.Any combination of 2 + 4 as defense-in-depth is also worth considering before committing to the larger option 1 or 3.
Relevant files
core-web/libs/sdk/experiments/src/lib/shared/parser/parser.tscore-web/libs/sdk/experiments/src/lib/dot-experiments.tscore-web/libs/sdk/experiments/src/lib/hooks/useExperimentVariant.tscore-web/libs/sdk/experiments/src/lib/hooks/useExperiments.tscore-web/libs/sdk/experiments/src/lib/standalone.tsdotCMS/src/main/java/com/dotcms/regex/MatcherTimeoutFactory.java.Acceptance Criteria
(a+)+bvs. a longaaaa…!string), does not freeze the browser tab. Matching either succeeds, is bounded by a configurable timeout, or is rejected before reachingRegExp.verifyRegexbecomingasync) are documented in the SDK changelog, and consumers (useExperimentVariant,useExperiments, standalone entry point) continue to function.parseDataForAnalytics,getVariantFromHref,getVariantAsQueryParam).Priority
Medium
Additional Context
/cmsHomePagescenario but does not add client-side ReDoS protection.