Skip to content

Commit 2392713

Browse files
authored
Merge pull request #3113 from hapijs/fix/link-max-call-stack
fix: protect link recursion from max call stack
2 parents f4e97e0 + fc146a6 commit 2392713

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

API.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2035,9 +2035,11 @@ Note that named links must be found in a direct ancestor of the link. The names
20352035
Links are resolved once (per runtime) and the result schema cached. If you reuse a link in different places, the first time it is resolved at run-time, the result will be used by all other instances. If you want each link to resolve relative to the place it is used, use a separate `Joi.link()` statement in each place or set the `relative()` flag.
20362036

20372037
::: warning
2038-
It is strongly advised to set a [`link.maxRecursion(limit)`](#linkmaxrecursionlimit) on recursive links to bound the validation depth and protect against deeply nested inputs.
2038+
It is strongly advised to set a [`link.maxRecursion(limit)`](#linkmaxrecursionlimit) on recursive links to bound the validation depth and protect against deeply nested inputs. As a safety net, when validation exceeds the runtime call stack while resolving a link, validation fails with the `link.depth` error code instead of crashing the process.
20392039
:::
20402040

2041+
Possible validation errors: [`link.depth`](#linkdepth)
2042+
20412043
Named links:
20422044

20432045
```js
@@ -2108,6 +2110,8 @@ const schema = Joi.object({
21082110
});
21092111
```
21102112

2113+
Possible validation errors: [`link.maxRecursion`](#linkmaxrecursion)
2114+
21112115
### `number`
21122116

21132117
Generates a schema object that matches a number data type (as well as strings that can be converted to numbers).
@@ -3952,6 +3956,21 @@ Additional local context properties:
39523956
}
39533957
```
39543958

3959+
#### `link.depth`
3960+
3961+
The validation chain exceeded the runtime call stack while resolving a recursive link. Returned instead of throwing a `RangeError`. Set [`link.maxRecursion(limit)`](#linkmaxrecursionlimit) to bound the depth deterministically.
3962+
3963+
#### `link.maxRecursion`
3964+
3965+
The link was entered more times in a single validation chain than the limit set via [`link.maxRecursion(limit)`](#linkmaxrecursionlimit).
3966+
3967+
Additional local context properties:
3968+
```ts
3969+
{
3970+
limit: number // Maximum number of times the link may be entered
3971+
}
3972+
```
3973+
39553974
#### `number.base`
39563975

39573976
The value is not a number or could not be cast to a number.

lib/types/link.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,19 @@ module.exports = Any.extend({
6565

6666
const linked = internals.generate(schema, value, state, prefs);
6767
const ref = schema.$_terms.link[0].ref;
68-
return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs);
68+
69+
try {
70+
return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs);
71+
}
72+
catch (err) {
73+
/* $lab:coverage:off$ */
74+
if (!(err instanceof RangeError)) {
75+
throw err;
76+
}
77+
/* $lab:coverage:on$ */
78+
79+
return { value, errors: error('link.depth') };
80+
}
6981
},
7082

7183
generate(schema, value, state, prefs) {
@@ -109,6 +121,7 @@ module.exports = Any.extend({
109121
},
110122

111123
messages: {
124+
'link.depth': '{{#label}} exceeds maximum recursion depth supported by the runtime',
112125
'link.maxRecursion': '{{#label}} exceeds maximum recursion depth of {{#limit}}'
113126
},
114127

test/types/link.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,26 @@ describe('link', () => {
397397
});
398398
});
399399

400+
describe('runtime stack overflow', () => {
401+
402+
it('reports a validation error instead of crashing on deeply nested recursive input', () => {
403+
404+
const schema = Joi.object({
405+
a: Joi.link('/')
406+
});
407+
408+
let value = {};
409+
for (let i = 0; i < 5000; ++i) {
410+
value = { a: value };
411+
}
412+
413+
const { error } = schema.validate(value);
414+
expect(error).to.exist();
415+
expect(error.details[0].type).to.equal('link.depth');
416+
expect(error.message).to.contain('exceeds maximum recursion depth supported by the runtime');
417+
});
418+
});
419+
400420
describe('when()', () => {
401421

402422
it('validates a schema with when()', () => {

0 commit comments

Comments
 (0)