Skip to content

Protect SDK verifyRegex from ReDoS (client-side catastrophic backtracking) #35379

@dsolistorres

Description

@dsolistorres

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 in core-web/libs/sdk/experiments/src/lib/shared/parser/parser.ts in the verifyRegex(regexToCheck, href) function and is called from:

  • parser.ts:35parseDataForAnalytics (tracker hot path; runs on every page view when experiments are active).
  • core-web/libs/sdk/experiments/src/lib/dot-experiments.ts:325getVariantFromHref.
  • core-web/libs/sdk/experiments/src/lib/dot-experiments.ts:344getVariantAsQueryParam.

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

  1. 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).
  2. 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).
  3. 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.
  4. 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.

Relevant files

  • core-web/libs/sdk/experiments/src/lib/shared/parser/parser.ts
  • core-web/libs/sdk/experiments/src/lib/dot-experiments.ts
  • core-web/libs/sdk/experiments/src/lib/hooks/useExperimentVariant.ts
  • core-web/libs/sdk/experiments/src/lib/hooks/useExperiments.ts
  • core-web/libs/sdk/experiments/src/lib/standalone.ts
  • 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions