d6a2b39252
```markdown
`social.parseHandlesFromHtml()` runs `emailsFromText()` on every text node of a
scraped page (`packages/utils/src/internals/social.ts`), and `emailsFromText()` /
`emailsFromUrls()` are exported directly. All of them match untrusted text against
`EMAIL_REGEX_GLOBAL`, built from `EMAIL_REGEX_STRING`.
The dot-atom local part `[...]+(?:\.[...]+)*` and each domain label
`[a-z0-9-]*[a-z0-9]` use unbounded quantifiers. On a long dotted or hyphenated
input that never completes a valid address, e.g. `x` + `.a`*n + `@` (no domain),
V8 explores every way to split the run and backtracks quadratically. Matching time
grows ~4x per input doubling (O(n^2)): a single ~60 KB text node blocks the event
loop for seconds, and it scales to tens of seconds on a larger page, so one crafted
crawled page can stall the crawler (the matching worker is single-threaded). This is
CWE-1333 (ReDoS).
The fix bounds those quantifiers with RFC 5321 lengths instead of leaving them
unbounded: local-part atoms `{1,64}`, dot-groups `{0,32}`, and the domain-label
inner run `{0,62}`. That makes the match linear while still accepting every
realistic address (a max-legal 64-char local part and 63-char label still match).
The quoted-string and IP-literal alternatives are unchanged. Only inputs longer than
what a valid email can contain are affected; matching against the original regex over
hundreds of thousands of random and structured strings showed no difference for any
in-range input.
A pure non-backtracking rewrite is not possible here: V8 has no atomic groups or
possessive quantifiers, and an anchored-lookahead form cannot be used because
`EMAIL_REGEX_GLOBAL` runs unanchored over scraped text.
Added an `emailsFromText()` regression test that feeds the adversarial inputs and
asserts they return `[]` well within a time budget; it fails on the old regex
(tens of seconds) and passes on the fixed one.
```
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>