visit
At , we make the fastest email experience in the world. We must therefore process massive amounts of text very rapidly. We need to find links, validate emails, parse invitations, and much more.
Most programmers process text with . And rightly so: regular expressions are concise yet powerful.But many programmers have also encountered the dark side of regular expressions. When regular expressions go wrong, they go devastatingly wrong.
In this case, we defined an email address as any string that matches this regular expression:
/([^@]*)@([^@]*)/
. You can read this as: “0 or more of any character except @, then an @ sign, then 0 or more of any character except @”.But while reading the of email addresses, we found some that did not match our regular expression. For example, if the username is quoted then it can also contain an @ sign. Consider
"joe@home"@example.com
.Now you might be thinking: “That’s crazy! Who would do that?!” But when you deal with user data, the unexpected happens all the time. Imagine you have 1 million users, and that each user has 50,000 emails. Between them, this is 50 billion emails. Even if it only happens once in every billion emails, that could still easily be 50 times!
Wanting to do the right thing, we naïvely changed our regular expression to. This is exactly the same as before, but also allows the username to contain 0 or more quoted sections./("[^"]*"|[^@])*@[^@]*/
It only took a few days before we received an email saying: “Please help! Superhuman is using 100% CPU and not responding…”We could see that the problem started after we changed this regular expression, and we could see that Superhuman broke on one email in particular. In some cases, the CPU would lock up for days.But why?It turns out that we had accidentally become vulnerable to (ReDoS).
Example state machine for /[^@]*@[^@]*/
This state machine has 3 states:
A
, B
, and $
. The machine starts in state A
. While here, the machine will match any character except @
and remain in state A
. If it encounters an @
sign, the machine will transition to state B
.In state, the machine will match any character exceptB
, and remain in state@
. At the end of the string, the machine transitions to stateB
.$
If the machine encountered anSo what changed when we updated our regular expression? The state machine for our second email matcher looks like this:sign while in state@
, it would error as there are no matching transitions. The error would show that the string did not match the regular expression.B
Example state machine for /("[^"]*"|[^@])*@[^@]*/
Do you see the problem?
It’s not obvious, but we introduced non-determinism. In state
A
, if the machine sees "
it has a choice: treat it as "
and transition to state C
, or treat it as any character except @
and stay in state A
.There are some to this problem, but JavaScript and most modern programming languages take a dangerous approach: when the state machine has multiple paths, it will just choose one and continue. If that choice leads to the entire string matching, the machine will stop. If that choice does not lead to a match, it will backtrack and try the next path.
In the worst case, the state machine has to try every single possible combination of options before it can determine that there is no match. And the number of options very quickly becomes huge. In our example, everyIf you don’t believe me, open your browser console and type:character doubles the number of possibilities. Our regular expression can therefore take O(2ⁿ) attempts to match, where n is the length of the string."
let regex = /("[^"]*"|[^@])*@([^@]*)/
t = performance.now()
regex.test('"""""""""""""""""""""""""""""""""""""""')
console.log(performance.now() - t) // about 3 seconds.
Each additional
"
character doubles the time required. This 40 character string takes about 3 seconds on a high-end MacBook Pro. A similar string of just 64 characters will take more than 2 years — assuming you don’t run out of battery in the meantime!In summary:Thanks to , , , , and for comments and suggestions on this post.
At Superhuman we’re rebuilding the email experience for web & mobile. Think vim / Sublime for email: blazingly fast and visually gorgeous.
If you enjoy working on user-facing problems that push existing solutions to their very limits — or beyond — join us! or .