ReDoS via string.pattern() with catastrophic-backtracking regex blocks event loop
- Dominant language
- JavaScript
- Stars
- 21.2k
- Forks
- 1.5k
- Avg merge
- 4h 57m
- Merged PRs (30d)
- 14
Description
### 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)
```javascript
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
```javascript
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=22
```
### Threat model
Two realistic attack scenarios:
1. **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.
2. **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:
1. **Synchronous timeout** via `vm.runInNewContext({timeout: N})` (rough, but blocks the event loop only for N ms).
2. **Re-implement matching with a Thompson-NFA backed engine** for joi's patterns (e.g. `re2` via `re2`/`re2-wasm`). Default to the safe engine; let users opt-in to native `RegExp` with a clear warning.
3. **At minimum**: detect catastrophic-backtracking patterns at `pattern(re)` registration time using static analysis (e.g. `safe-regex` or `recheck`) 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+
Contributor guide
Assessment
This issue has not been assessed yet.