VYPR
Medium severity5.3GHSA Advisory· Published Jun 11, 2026· Updated Jun 11, 2026

joi has an uncaught RangeError on deeply nested input through recursive `link()` schemas

CVE-2026-48038

Description

Uncaught RangeError in Joi when validating deeply nested input with recursive link() schemas can cause denial of service.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Uncaught RangeError in Joi when validating deeply nested input with recursive link() schemas can cause denial of service.

Vulnerability

In Joi versions before 18.2.1, validating user-supplied JSON or object input with recursive link() schemas can cause an untrapped RangeError due to call stack overflow [1][2][3]. The issue affects all versions prior to the fix.

Exploitation

An attacker can send deeply nested input that triggers recursion in link() schemas, exceeding the runtime call stack. If validate() is called without try/catch in a request handler, the unhandled exception can crash the process. Even when using try/catch, the error is a RangeError instead of a structured ValidationError, complicating error handling [3][4].

Impact

Denial of service: an unhandled exception can crash the application. In cases where try/catch is used, validation fails with a RangeError, disrupting normal error flow and requiring non-standard handling.

Mitigation

Upgrade to Joi >= 18.2.1, which catches the recursion and returns a link.depth error code instead of throwing [1][2][3]. As a workaround, ensure all validation calls are wrapped in try/catch to prevent uncaught exceptions [3][4].

AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
2392713d3e9d

Merge pull request #3113 from hapijs/fix/link-max-call-stack

https://github.com/hapijs/joiNicolas MorelMay 4, 2026via body-scan-shorthand
3 files changed · +54 2
  • API.md+20 1 modified
    @@ -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.
    
  • lib/types/link.js+14 1 modified
    @@ -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}}'
         },
     
    
  • test/types/link.js+20 0 modified
    @@ -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()', () => {
    

Vulnerability mechanics

Root cause

"Missing exception handling around recursive link validation allows a `RangeError` to propagate uncaught, causing a denial of service."

Attack vector

An attacker sends a JSON payload with deeply nested objects that reference a recursive `Joi.link()` schema. Because the link is resolved recursively during validation, a sufficiently deep nesting causes a `RangeError` (stack overflow). If the application calls `validate()` without a `try/catch` in a request handler, this unhandled exception can crash the process, resulting in a denial of service [ref_id=1].

Affected code

The vulnerability resides in `lib/types/link.js` where `$_validate` is called without protection against `RangeError` thrown when recursive link resolution exceeds the runtime call stack. The patch adds a `try/catch` around that call and returns a structured `link.depth` validation error instead of letting the exception propagate.

What the fix does

The patch wraps the recursive `$_validate` call in `lib/types/link.js` inside a `try/catch` block. When a `RangeError` is caught, it returns a `link.depth` validation error instead of throwing. This prevents the unhandled exception from crashing the process and gives callers a structured error they can handle gracefully [patch_id=5594804].

Preconditions

  • configThe application uses Joi schemas with `Joi.link()` that reference themselves recursively (e.g., `Joi.object({ a: Joi.link('/') })`).
  • inputThe attacker can supply input with deeply nested objects (e.g., 5000 levels of nesting) that trigger the recursive link resolution.
  • configThe application does not set a `link.maxRecursion()` limit on the schema, or the limit is set high enough that the runtime call stack is exhausted first.

Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.