DEV Community

Guo
Guo

Posted on

Regex Testers Can Freeze the Browser: Safer Testing with Timeouts

A regex can look harmless in code review and still lock up a page when someone pastes the wrong input.

Most of us learn regular expressions as a compact way to validate a field, find a log line, or clean up text. The trouble starts when a pattern gives the engine many ways to interpret the same characters. If the match finally fails near the end of a long string, a backtracking engine may retry a huge number of possibilities before it gives up.

That is how a small regex turns into a CPU problem.

OWASP calls this Regular Expression Denial of Service, or ReDoS. The risk is not limited to servers. A bad pattern can block a browser tab, slow a worker, or tie up an API process if untrusted input reaches it. OWASP's ReDoS overview includes examples of patterns that grow dramatically slower on carefully chosen non-matching input.

The pattern that looks innocent

Consider this regex:

/^(a+)+$/
Enter fullscreen mode Exit fullscreen mode

It accepts one or more a characters. The nested + quantifiers create the problem.

For a normal input, it seems fine:

/^(a+)+$/.test("aaaaaa"); // true
Enter fullscreen mode Exit fullscreen mode

Now try a near miss:

const pattern = /^(a+)+$/;
const input = "a".repeat(30) + "!";

pattern.test(input);
Enter fullscreen mode Exit fullscreen mode

The final ! prevents a match. Before the engine can return false, it may try many ways to divide the preceding a characters across the inner and outer repetitions.

The issue is not that + is always dangerous. The issue is ambiguity. If two repeated parts can consume the same characters, the engine has multiple routes to explore when the match fails.

OWASP lists nested repetition and overlapping alternatives as common warning signs:

(a+)+
(a*)*
(a|aa)+
([a-zA-Z]+)*$
Enter fullscreen mode Exit fullscreen mode

A warning is not a proof that a regex is exploitable. It is a signal to test the pattern with realistic and hostile inputs before it reaches production.

The browser can be part of the failure

JavaScript regular expressions run synchronously when you call methods such as test, exec, match, or replace. If a costly match runs on the main thread, the UI cannot repaint or respond to input until that work finishes.

A test tool should isolate this work where possible. A Web Worker can run the regex outside the page's main UI thread. If the test does not finish within a bounded time, the page can terminate that worker and show a useful error instead of waiting forever.

That protects the testing page. It does not prove the regex is safe in your application.

A Node.js process, a Java service, a Python worker, and a browser tab may use different engines, time limits, input limits, and resource controls. Test the pattern where it will actually run.

Rewrite the pattern around the rule you mean

The best fix is usually to remove the ambiguity.

Suppose you want to allow words separated by spaces.

This pattern has nested repetition:

/^(\w+\s?)*$/
Enter fullscreen mode Exit fullscreen mode

A clearer version describes the structure directly:

/^\w+(?:\s+\w+)*$/
Enter fullscreen mode Exit fullscreen mode

This says:

  1. Match one word.
  2. Then match zero or more occurrences of one or more spaces followed by another word.

The second version also makes the intended rule easier to explain in a code review.

Other practical safeguards help:

  • Put a maximum length on input before matching it.
  • Prefer fixed, reviewed patterns over patterns assembled from user input.
  • Test inputs that almost match but fail near the end.
  • Avoid nested unbounded quantifiers when the repeated parts overlap.
  • Apply server-side limits too. Client-side validation does not protect an API.

The JavaScript RegExp API is flexible, especially when patterns are built dynamically with the RegExp() constructor. That flexibility deserves care when either the pattern or the text comes from an untrusted source. MDN's RegExp reference documents the API and its constructor behavior.

Test the happy path and the near miss

A regex test case should include more than an example that matches.

For a username pattern, test:

good_name
good-name
too short
a very long value
a value that fails at the final character
Enter fullscreen mode Exit fullscreen mode

For a log parser, test long lines with one bad suffix. For a URL validator, test long repeated fragments and malformed delimiters. The inputs that fail late are often more informative than the inputs that pass immediately.

A timeout is a guardrail, not a certificate

I built the ToolExo Regex Tester to run JavaScript regex evaluations in a disposable Web Worker. The tool terminates work that exceeds 500 milliseconds and flags a few risky shapes, such as nested repetition and repeated alternatives with overlapping branches.

The timeout keeps one experiment from trapping the tester. It does not certify a pattern as safe, and it does not transfer its protection to another runtime.

Use the tool to inspect matches, capture groups, replacements, and difficult test cases. Then test the same pattern with your production input limits and runtime configuration.

Keep the rule simple enough to defend

A regular expression is easier to maintain when you can explain what each repeated section consumes and why it cannot compete with another section for the same input.

If that explanation becomes hard, split the job:

  1. Use a simple regex to check broad syntax.
  2. Parse or validate the meaningful parts in ordinary code.
  3. Enforce length and business rules separately.

This approach often produces clearer error messages as well. A user learns whether a value is too long, has an invalid character, or fails a business rule. They do not get a generic "invalid input" message from a pattern nobody wants to edit.

Regex remains useful. It just needs the same care we give database queries, file parsers, and every other piece of code that processes untrusted input.

Top comments (0)