As a tech lead, how would you detect and prevent ReDoS vulnerabilities across a Java codebase?
answer
- inventory: Pattern.compile / matches / @Pattern on untrusted data
- detect in CI: CodeQL/Sonar/ReDoS lints + adversarial fuzz tests
- no user patterns on default engine -> RE2J (linear time)
- possessive/atomic + bound input length
- time-box: cancellable thread or throwing CharSequence
- policy: shared lib, secure-coding guide, PR checklist
basics
~20 sFind risky patterns with linters and code review, test patterns against malicious inputs, never let users supply patterns to the default engine, and run untrusted matching with a timeout or a linear-time engine like RE2J so no single regex can hang a thread.
solid answer
~50 sI treat ReDoS as a systemic risk, not a one-off bug. Detection: enable static analysis (SonarQube, CodeQL, or dedicated ReDoS linters) that flag nested/overlapping quantifiers; add a unit-test convention that runs each non-trivial pattern against a crafted catastrophic input under a tight timeout, failing the build if it blows up; and inventory every place user input touches a regex. Prevention layered: (1) forbid compiling user-supplied patterns on java.util.regex — if dynamic patterns are a product need, route them through RE2J, which guarantees linear time; (2) refactor flagged patterns to possessive quantifiers/atomic groups and bound input length before matching; (3) for any untrusted matching, enforce a hard time budget by running on a cancellable thread or a deadline-checking CharSequence; (4) add WAF/rate-limit defense in depth. I also bake the rules into a shared validation library and the secure-coding guide so the safe path is the default.
go deeper
Can fix one flagged pattern with possessive quantifiers and add a test for a malicious input when guided.
Knows the mitigation menu (refactor, bound input, timeout) and can add input-length limits and avoid user-supplied patterns in their own code.
Designs the per-feature defense: chooses RE2J vs timeout vs rewrite, and writes adversarial tests, but may not own org-wide rollout.
Owns the systemic program: CI detection, RE2J platform mandate for dynamic patterns, shared validation libraries, secure-coding policy, review gates, and weighing trade-offs (feature loss, false positives) across teams.
## Framing: ReDoS is a class of bug, not an incident A tech lead's job is to make the whole codebase resistant, so a junior can't reintroduce the problem next sprint. That means **detection, prevention, and policy**, in layers. ## 1. Inventory the attack surface First, find where regex meets untrusted data. Two cases (from the ReDoS question): - **User-supplied PATTERN** — search filters, rules engines, any `Pattern.compile(userInput)`. Highest risk. - **User-supplied INPUT to a fixed pattern** — validators for email/URL/phone/dates, log/header parsers. Grep for `Pattern.compile`, `String.matches`, `replaceAll`, `split`, and Bean Validation `@Pattern`. Map each to whether its pattern and/or its input is attacker-influenced. ## 2. Detect vulnerable patterns automatically - **Static analysis:** SonarQube has ReDoS rules; **CodeQL** has queries for polynomial/exponential regex; dedicated tools/libraries can analyze a pattern's worst-case complexity. Wire these into CI so a risky pattern fails the build. - **Pattern-shape lint:** flag nested quantifiers `(x+)+`, `(x*)*`, overlapping alternations `(a|a)*`, `(a|ab)*`, and adjacent overlapping quantifiers `\s*\s*`. - **Property/fuzz testing:** generate adversarial 'matching-prefix + breaker' strings and assert the match completes under, say, 50 ms. A regression that introduces backtracking then fails CI. ## 3. Prevent — defense in depth (most to least preferred) 1. **Eliminate user-supplied patterns on the default engine.** Treat a user pattern as executable code. If the product truly needs dynamic patterns (e.g. a search DSL), compile them with **RE2J** — a Java port of Google's RE2 that runs in **time linear in input length** and cannot backtrack (trade-off: no backreferences/some lookaround). 2. **Fix the patterns.** Refactor flagged regexes using **possessive quantifiers** (`a++`) and **atomic groups** (`(?>...)`); prefer character classes over alternations; anchor patterns; avoid `.*` where a specific class works. 3. **Bound the input** *before* matching — reject inputs longer than a sane maximum, since exponential cost needs length to bite. 4. **Time-box untrusted matching.** java.util.regex has **no native timeout**, so run the match on a **cancellable thread** (interrupt after a deadline) or wrap the input in a **`CharSequence` whose `charAt` throws** once a time budget elapses — `Matcher` reads `charAt` while scanning, so it aborts. 5. **Perimeter defenses:** WAF rules, request rate limiting, per-request CPU budgets, and on reactive stacks, never run regex on the event-loop thread. ## 4. Codify it as policy - Put the rules in the **secure-coding guide** and a **shared validation library** so the safe constructs are the default and copy-paste path. - Add a **PR review checklist** item: 'any new regex on untrusted data — is it bounded, possessive/atomic, or RE2J?' - Provide a **golden set of vetted patterns** (email, URL, etc.) so teams don't hand-roll vulnerable ones. - **Architecture choice:** for any feature accepting user patterns, mandate RE2J at the platform level rather than per-team discretion. ## 5. Trade-offs to weigh - RE2J buys safety but drops backreferences/lookbehind — confirm features in use don't depend on them. - Timeouts add thread-management complexity and can mask a genuinely slow legitimate pattern; pair with monitoring. - Aggressive input bounds can reject legitimate large inputs; size them from real data. ## First-principles summary Make the safe path the default and the unsafe path impossible-by-default: inventory where regex meets untrusted data, detect risky shapes in CI (CodeQL/Sonar/fuzz), and prevent in layers — no user patterns on the default engine (use RE2J), possessive/atomic rewrites, input bounds, and time-boxed execution — then encode all of it in shared libs, guides, and review gates.
- What is the single highest-leverage control if you can only do one thing?Never compile user-supplied patterns on java.util.regex — route any dynamic pattern through a linear-time engine like RE2J. That removes the most dangerous attack surface (attacker-controlled pattern) outright.
- How do you stop a regression from reintroducing a vulnerable pattern after you've fixed everything?Gate CI with static analysis (CodeQL/Sonar ReDoS rules) plus a fuzz/property test that runs each pattern against adversarial inputs under a tight timeout, and add a PR-review checklist item — so a new bad pattern fails the build, not production.
saying these in an interview costs you the question
- Treating ReDoS as a single bug to patch rather than a class to prevent systemically
- Relying solely on a WAF without fixing patterns or bounding input
- Allowing user-supplied patterns on java.util.regex 'because we validate them'
- Assuming RE2J is a drop-in with zero feature loss (it drops backreferences/some lookaround)