Inception
The inception of this research was kind of a silly story. Basically I was doing an internal audit of one of my employer’s projects, and through the well-known roller coaster a researcher lives in during an audit, I came across this ReDoS attack. DoSes are not commonly of interest for a hacker that wishes for something more entertaining like an RCE, Account Takeovers and others, while on the other hand, a script kiddie might be interested in this family of attacks —non-interestingly, the DDoS variant. Rant aside, the ReDoS variant seemed more intellectually engaging than the others. This led us (me, Ayoub, Mokhtari) to find this variant in a multitude of software, specifically in the JS world (no surprise ¯_(ツ)_/¯), and we hope this is a warning call for a more security-aware, wide-view development and assessment).
If I have seen further [than others], it is by standing on the shoulders of giants. - Newton
These findings wouldn’t been possible for what previous researchers have done before us, especially the USENIX Security ‘18 - “Freezing the Web: A Study of ReDoS Vulnerabilities in JavaScript-based Web Servers” research done by Cristian Alexandru Staicu and Michael Pradel, TU Darmstadt, and the fascinating ReCheck tool made by “MakeNowJust” that helped us evaluate every regex and generate every PoC.
Findings
libphonenumber-js (≈ 12M weekly downloads)
One of the first target was the libphonenumber-js package, we didn’t got to it directly, it started with a NestJS web application that uses class-validator’s IsPhoneNumber decorator which in turn uses libphonenumber-js’s parsePhoneNumber function, this was the source, now to find the vulnerable we have to dig deeper.
Digging deeper we found that libphonenumber-js handles a not so familiar -for me the least- syntax which is the tel URI (e.g tel:555-1234) defined in the RFC 3966:

RFC3966_DOMAINNAME_PATTERN was dynamically built but the final regex looked as the following:

This regex was exponential, but it only applied to the “context” of the number, which turns out it could be appended to the number e.g tel:555-1234;phone-context:example.com.
Couldn’t be easier.

PoC
Since this package is used by packages like class-validator, that’s used by NestJS framework, our PoC uses that former package but it could also work with other dependencies.
Disclosure
We contacted the maintainer, he made it clear he will continue to ignore this issue, to his point, he seemed to have copied the logic of Google’s libphonenumber (which seems to have the same bug —we didn’t bother to check) library, fair enough, we emailed a class-validator member and we still didn’t have a response, moving on to the next one.
firebase-admin (≈ 4M weekly downloads)
This was rather a simple one to identify, the library’s utility function isURL has a vulnerable regex easily hanged by an input as small as (https://0000000000000000000000000000$):
if (!hostname || !/^[a-zA-Z0-9]+[\w-]*([.]?[a-zA-Z0-9]+[\w-]*)*$/.test(hostname)) {
return false;
}
There are a lot of sources to this sink, for instance, admin.auth().updateUser commonly called by the package users to update the user’s profile picture among other fields.
So basically any backend that utilizes firebase-admin SDK and that lets the user customize their imageURL would be vulnerable to an attacker freezing the server, e.g:
await admin.auth().updateUser(uid, {
phoneNumber: phoneNumber,
photoURL: photoURL
});
PoC
Disclosure
We disclosed the vulnerability to Google VRP and it was acknowledged by them, in fact, it was patched —silently— just last week (as of the time of writing). Here is the patch commit/pull request #3061.
node-forge (≈ 29M weekly downloads)
This was a clear one too, the rMessage (pem.js:99) in the forge.pem.decode function regex was also vulnerable but not exponential as the above ones (i.e. it need a bigger input):
// ...
var rMessage = /\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\x21-\x7e\s]+?(?:\r?\n\r?\n))?([:A-Za-z0-9+\/=\s]+?)-----END \1-----/g;
var rHeader = /([\x21-\x7e]+):\s*([\x21-\x7e\s^:]+)/;
var rCRLF = /\r?\n/;
var match;
while(true) {
match = rMessage.exec(str);
if(!match) {
break;
}
// ...
PoC
import forge from 'node-forge';
const pemString = ' -----BEGIN I-----G-----END B'.repeat(800) + '\x00';
// const pemObjects = forge.pki.privateKeyFromPem(pemString);
const pemObjects1 = forge.pem.decode(pemString);
The function in question (forge.pem.decode / forge.pki.privateKeyFromPem) has been used in multiple OSS repositories, in fact we got here because this function was used by one of firebase-admin SDK functions, but it wasn’t provided with user input directly.
Disclosure
This vulnerability was disclosed with the maintainers, with a suggested idea of a patch, but as of the time of writing, a patch/advisory is yet to be published.
ThreeJS (≈ 4M weekly downloads)
The pat3Floats (VTKLoader.js:116) function in ThreeJS was also vulnerable to medium sized inputs e.g '0'.repeat(331) + '0'.repeat(331) + '0\t0' + '0'.repeat(331) + '0'.
The vulnerable snippet of code is the following:
// ...
const pat3Floats = /(\-?\d+\.?[\d\-\+e]*)\s+(\-?\d+\.?[\d\-\+e]*)\s+(\-?\d+\.?[\d\-\+e]*)/g;
// ...
while ( ( result = pat3Floats.exec( line ) ) !== null ) {
if ( patWord.exec( line ) !== null ) break;
const x = parseFloat( result[ 1 ] );
const y = parseFloat( result[ 2 ] );
const z = parseFloat( result[ 3 ] );
positions.push( x, y, z );
}
// ...
PoC
import { TextEncoder } from "node:util";
import { VTKLoader } from 'three/addons/loaders/VTKLoader.js';
const payload =
'0'.repeat(220) +
'0'.repeat(220) +
'0\t0' +
'0'.repeat(220) +
'0';
const text =
`# vtk DataFile Version 3.0
ReDoS Test
ASCII
DATASET POLYDATA
POINTS 1 float
${payload}
`;
const encoder = new TextEncoder();
const arrayBuffer = encoder.encode(text).buffer;
new VTKLoader().parse(arrayBuffer);
An attacker can cause the hanging of the VTKLoader.parse function thus the server indefinitely when parsing a malicious .vtk file or whatever scenario this function is triggered in.
Disclosure
This was promptly fixed by the ThreeJS maintainer some weeks ago, (pull request).
Not all regexes are dangerously reachable
Of course it’s not the case that all regexes are vulnerable, and furthermore, your input will not always get to the vulnerable regex, an active case is the code of the new Coinbase x402 protocol library implementation, it has a vulnerable regex but the character (/) repetition required to apply the DoS is previously removed (watch out for this, it might change 👀):

Conclusion
These ReDoS issues stem from regex patterns prone to catastrophic backtracking, multiple overlapping quantifiers (e.g. nested +/* or .* followed by specific matchers) create exponential execution paths on adversarial near-matching inputs. JavaScript’s single-threaded nature turns seconds-long regex evaluation into full process hangs, that’s what makes them very dangerous, we think this issue is not that hard to fix (just some transpilation-time linting using the recheck library for example will prevent most of them, just like the compilation checks that’s been added this last decade for printf-family format strings attacks) but we don’t necessarily think that the adoption will be fast, again this research has been done about a decade ago, but we still were able to find the same very easy to detect bug in recent software, and it’s in many other software, how do we know you say? it’s because remember the project’s I’ve been auditing, all of the first three bugs were in it, I just had to script-kid a script to detect the vulnerable regexes in the node_modules of that project, imagine if did a more broad scan, we hope this served as a warning call, thank you for reading!