Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2035,9 +2035,11 @@ Note that named links must be found in a direct ancestor of the link. The names
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.

::: warning
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.
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.
:::

Possible validation errors: [`link.depth`](#linkdepth)

Named links:

```js
Expand Down Expand Up @@ -2108,6 +2110,8 @@ const schema = Joi.object({
});
```

Possible validation errors: [`link.maxRecursion`](#linkmaxrecursion)

### `number`

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

#### `link.depth`

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.

#### `link.maxRecursion`

The link was entered more times in a single validation chain than the limit set via [`link.maxRecursion(limit)`](#linkmaxrecursionlimit).

Additional local context properties:
```ts
{
limit: number // Maximum number of times the link may be entered
}
```

#### `number.base`

The value is not a number or could not be cast to a number.
Expand Down
15 changes: 14 additions & 1 deletion lib/types/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,19 @@ module.exports = Any.extend({

const linked = internals.generate(schema, value, state, prefs);
const ref = schema.$_terms.link[0].ref;
return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs);

try {
return linked.$_validate(value, state.nest(linked, `link:${ref.display}:${linked.type}`), prefs);
}
catch (err) {
/* $lab:coverage:off$ */
if (!(err instanceof RangeError)) {
throw err;
}
/* $lab:coverage:on$ */

return { value, errors: error('link.depth') };
}
},

generate(schema, value, state, prefs) {
Expand Down Expand Up @@ -109,6 +121,7 @@ module.exports = Any.extend({
},

messages: {
'link.depth': '{{#label}} exceeds maximum recursion depth supported by the runtime',
'link.maxRecursion': '{{#label}} exceeds maximum recursion depth of {{#limit}}'
},

Expand Down
20 changes: 20 additions & 0 deletions test/types/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,26 @@ describe('link', () => {
});
});

describe('runtime stack overflow', () => {

it('reports a validation error instead of crashing on deeply nested recursive input', () => {

const schema = Joi.object({
a: Joi.link('/')
});

let value = {};
for (let i = 0; i < 5000; ++i) {
value = { a: value };
}

const { error } = schema.validate(value);
expect(error).to.exist();
expect(error.details[0].type).to.equal('link.depth');
expect(error.message).to.contain('exceeds maximum recursion depth supported by the runtime');
});
});

describe('when()', () => {

it('validates a schema with when()', () => {
Expand Down
Loading