hapijs / hapijs/joi

ReDoS via string.pattern() with catastrophic-backtracking regex blocks event loop

Open
#3,119 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.