diff --git a/API.md b/API.md index d8e861ed..3b65bae2 100755 --- a/API.md +++ b/API.md @@ -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 @@ -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). @@ -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. diff --git a/lib/types/link.js b/lib/types/link.js index e7b59776..e3cd1b8a 100755 --- a/lib/types/link.js +++ b/lib/types/link.js @@ -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) { @@ -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}}' }, diff --git a/test/types/link.js b/test/types/link.js index 26e39cc8..727b5464 100755 --- a/test/types/link.js +++ b/test/types/link.js @@ -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()', () => {