Replace catastrophically backtracking email/URL patterns with safe alternatives #153

Merged
tobi merged 2 commits from fix-redos-string-patterns into main 2026-07-03 08:13:49 +00:00
Owner

Problem

String.EMAILPATTERN and String.URLPATTERN (Scott Gonzalez, 2009) are textbook ReDoS vulnerabilities. Their domain-matching section used an alternation (A|B) where alternative B included . in its middle-character class:

([a-z]|\d|-|.|_|~|…)*   ← dot allowed here

This made the label-separator . ambiguous: a dot could be consumed either inside a label by alternative B, or between labels by the outer \. separator. For a hostname with n dot-separated labels, Rhino's backtracking NFA had to explore 2ⁿ distinct parse paths when the overall match failed (e.g. an invalid TLD). With ~15 labels the thread spins for days.

See antville/antville#229, where 9 request threads were each stuck in NativeRegExp.executeREBytecode for up to 18 800 minutes (~13 days).

Fix

Exclude . from the label character classes. Each dot then has exactly one role (separator), the parse tree is linear, and backtracking is O(n). ! is also excluded from domain/hostname labels — it is not a valid DNS label character (RFC 952/1123), though it remains permitted in the email local part (before @) per RFC 5322.

// before — '."' in middle-chars creates 2ⁿ parse paths
([a-z]|\d|-|.|_|~|)*[a-z]

// after — '".' and '!' excluded from domain/hostname labels
[^\s@.!]+

The new patterns:

String.EMAILPATTERN = /^[^\s@]+@[^\s@.!]+(?:\.[^\s@.!]+)+$/;
String.URLPATTERN   = /^(?:https?|ftp):\/\/(?:[^\s@\/?#]+@)?[^\s.\/\?#:!]+(?:\.[^\s.\/\?#:!]+)*(?::\d+)?(?:\/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?$/i;

Unicode and IDN are fully supported: [^\s@.!] passes every non-ASCII character unchanged. The .test() interface is preserved.

Testing (verified on antville-test.de)

  • user@example.com, a@b.io, user+tag@sub.domain.co.uk → valid ✓
  • user!name@example.com (! in local part) → valid ✓
  • 用户@例子.广告 (Chinese Unicode email) → valid ✓
  • user@[1.2.3.4] (IPv4 literal), user@10.0.0.1 (bare IPv4) → valid ✓
  • notanemail, user@, @domain, user@domain (no TLD) → invalid ✓
  • user@[IPv6:2001:db8::1] (IPv6 literal) → invalid ✓
  • user@invalid! (! in domain) → invalid ✓
  • http://example.com, https://例子.广告/path (IDN URL) → valid ✓
  • http://example.com/path!here (! in path) → valid ✓
  • javascript:alert(1), http://, http://exa!mple.com (! in host) → invalid ✓
  • ReDoS attack user@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.invalid! → 83 ms, correctly rejected ✓
## Problem `String.EMAILPATTERN` and `String.URLPATTERN` (Scott Gonzalez, 2009) are textbook ReDoS vulnerabilities. Their domain-matching section used an alternation `(A|B)` where alternative B included `.` in its middle-character class: ``` ([a-z]|\d|-|.|_|~|…)* ← dot allowed here ``` This made the label-separator `.` ambiguous: a dot could be consumed either *inside* a label by alternative B, or *between* labels by the outer `\.` separator. For a hostname with *n* dot-separated labels, Rhino's backtracking NFA had to explore **2ⁿ** distinct parse paths when the overall match failed (e.g. an invalid TLD). With ~15 labels the thread spins for days. See [antville/antville#229](https://code.host.antville.org/antville/antville/issues/229), where 9 request threads were each stuck in `NativeRegExp.executeREBytecode` for up to 18 800 minutes (~13 days). ## Fix Exclude `.` from the label character classes. Each dot then has exactly one role (separator), the parse tree is linear, and backtracking is O(n). `!` is also excluded from domain/hostname labels — it is not a valid DNS label character (RFC 952/1123), though it remains permitted in the email local part (before `@`) per RFC 5322. ```js // before — '."' in middle-chars creates 2ⁿ parse paths ([a-z]|\d|-|.|_|~|…)*[a-z] // after — '".' and '!' excluded from domain/hostname labels [^\s@.!]+ ``` The new patterns: ```js String.EMAILPATTERN = /^[^\s@]+@[^\s@.!]+(?:\.[^\s@.!]+)+$/; String.URLPATTERN = /^(?:https?|ftp):\/\/(?:[^\s@\/?#]+@)?[^\s.\/\?#:!]+(?:\.[^\s.\/\?#:!]+)*(?::\d+)?(?:\/[^\s]*)?(?:\?[^\s]*)?(?:#[^\s]*)?$/i; ``` Unicode and IDN are **fully supported**: `[^\s@.!]` passes every non-ASCII character unchanged. The `.test()` interface is preserved. ## Testing (verified on antville-test.de) - `user@example.com`, `a@b.io`, `user+tag@sub.domain.co.uk` → valid ✓ - `user!name@example.com` (`!` in local part) → valid ✓ - `用户@例子.广告` (Chinese Unicode email) → valid ✓ - `user@[1.2.3.4]` (IPv4 literal), `user@10.0.0.1` (bare IPv4) → valid ✓ - `notanemail`, `user@`, `@domain`, `user@domain` (no TLD) → invalid ✓ - `user@[IPv6:2001:db8::1]` (IPv6 literal) → invalid ✓ - `user@invalid!` (`!` in domain) → invalid ✓ - `http://example.com`, `https://例子.广告/path` (IDN URL) → valid ✓ - `http://example.com/path!here` (`!` in path) → valid ✓ - `javascript:alert(1)`, `http://`, `http://exa!mple.com` (`!` in host) → invalid ✓ - ReDoS attack `user@a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.invalid!` → 83 ms, correctly rejected ✓
String.EMAILPATTERN and String.URLPATTERN (Scott Gonzalez, 2009) allowed '.'
inside domain label middle-chars, making the dot-separator ambiguous with the
outer label-separator in Rhino's backtracking NFA engine. On input like
"user@a.b.c.d.e.f.invalid!" the engine explores 2ⁿ parse paths — enough
labels lock a request thread permanently (antville/antville#229).

Replace both patterns:
- EMAILPATTERN: HTML5 spec pattern — single character class for local part,
  bounded {0,61} label quantifiers for domain, no '.' inside label matches.
- URLPATTERN: scheme://userinfo?host(:port)?(/path)?(?query)?(#frag)? using
  the same bounded label structure for the hostname section.

Both patterns are O(n) for any input length.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177qdRkssL6kBk5ngMDHwnP
tobi force-pushed fix-redos-string-patterns from a54e8af67f to 69cfc85f10 2026-07-02 15:39:50 +00:00 Compare
'!' is not a valid DNS label character (RFC 952/1123) so it should be
rejected in both email domains and URL hostnames. It remains allowed in
the email local part where RFC 5322 permits it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177qdRkssL6kBk5ngMDHwnP
tobi merged commit 497572d9f4 into main 2026-07-03 08:13:49 +00:00
tobi self-assigned this 2026-07-03 20:49:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
antville/helma!153
No description provided.