ReDoS via string.pattern() with catastrophic-backtracking regex blocks event loop
Description
Joi.string().pattern(re) uses RegExp.test() directly with no safety wrapper. When the schema developer passes (or accepts at runtime) a regex with nested quantifiers (e.g. ((a+)+)+$), validation of even short attacker-controlled strings blocks the event loop for seconds-to-minutes — classic Regular-Expression Denial-of-Service (ReDoS).
Reproduction (joi 17.13.3)
const Joi = require('joi');
const schema = Joi.string().pattern(/((a+)+)+$/);
const input = 'a'.repeat(22) + '!'; // 23 chars
const t0 = Date.now();
schema.validate(input);
console.log('elapsed', Date.now() - t0, 'ms');
// elapsed ≈ 5000ms (blocking the event loop)Each additional a roughly doubles the time:
| input length | elapsed |
|---|---|
18 + ! |
~ 300 ms |
20 + ! |
~ 1.2 s |
22 + ! |
~ 5 s |
24 + ! |
~ 20 s |
28 + ! |
~ 5 min |
Patterns like (a*)*$, (a|aa)*$, (a|a?)*$ exhibit the same behavior.
Property that fails
import fc from "fast-check";
import Joi from "joi";
// A schema developer composes a pattern from validated parts; whether the
// resulting regex is ReDoS-safe is not obvious at compile time.
fc.assert(fc.property(
fc.integer({min: 1, max: 30}),
(n) => {
const schema = Joi.string().pattern(/((a+)+)+$/);
const t0 = Date.now();
schema.validate("a".repeat(n) + "!");
return (Date.now() - t0) < 1000; // < 1 s for any small input
}
));
// Shrinks to n=22Threat model
Two realistic attack scenarios:
- Public regex-config endpoint: An API lets users specify a "name pattern", "URL pattern", or similar custom validation regex (joi-based JSON Schema admin tools, low-code form builders). A malicious user supplies a catastrophic-backtracking pattern; subsequent requests that go through it hang the server.
- Static schema using user-supplied regex built from user input (e.g.
Joi.string().pattern(new RegExp(req.body.pattern))): direct DoS on every request that flows through that handler.
Even for schemas that hard-code the regex, third-party schema libraries that wrap user input into joi patterns inherit the issue.
Suggested fix
Wrap the regex.test(value) call with one of:
- Synchronous timeout via
vm.runInNewContext({timeout: N})(rough, but blocks the event loop only for N ms). - Re-implement matching with a Thompson-NFA backed engine for joi's patterns (e.g.
re2viare2/re2-wasm). Default to the safe engine; let users opt-in to nativeRegExpwith a clear warning. - At minimum: detect catastrophic-backtracking patterns at
pattern(re)registration time using static analysis (e.g.safe-regexorrecheck) and either reject or warn.
Yup, Zod, and ajv have all converged on documenting the ReDoS concern; many of them ship a format: re2 option. joi documents nothing here.
Environment
- joi: 17.13.3
- Node: 20+
Source: hapijs/joi