Aikido

Why you should avoid recursion without depth protection

Bug Risk

Rule
Avoid recursion without depth protection.
Recursion without proper depth limiting risks stack
overflow and creates DoS vulnerabilities from malicious
input. Recursion with enforced depth limits and proper
bounds checking is acceptable.

Language support: 45+

Introduction

Recursive functions without depth limits can exhaust the call stack, causing crashes. Malicious input like deeply nested JSON objects or cyclic data structures can trigger unbounded recursion intentionally. A single crafted request can crash your service by exceeding stack limits, creating a denial-of-service vulnerability that's trivial to exploit.

Why it matters

Security implications (DoS attacks): Attackers can craft input that triggers deep recursion, crashing your application. Deeply nested JSON, XML, or linked data structures are common attack vectors. A single malicious request exhausts the stack, taking down the entire service for all users.

System stability: Stack overflow errors crash the process immediately without graceful degradation. In production, this means dropped requests, interrupted transactions, and service unavailability. Recovery requires restarting the entire application.

Resource exhaustion: Unbounded recursion consumes stack memory exponentially. Each recursive call adds a stack frame, and deep recursion chains can consume megabytes of memory. This affects other processes on the same server and can trigger out-of-memory conditions.

Code examples

❌ Non-compliant:

function processNestedData(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

    const result = {};
    for (const key in obj) {
        result[key] = processNestedData(obj[key]);
    }
    return result;
}

Why it's wrong: No depth limit allows attackers to send deeply nested objects that exceed stack limits. Input like {a: {a: {a: {...}}}} nested 10,000 levels deep crashes the application with stack overflow. The function blindly recurses without checking depth.

✅ Compliant:

function processNestedData(obj, depth = 0, maxDepth = 100) {
    if (depth > maxDepth) {
        throw new Error('Maximum nesting depth exceeded');
    }

    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }

    const result = {};
    for (const key in obj) {
        result[key] = processNestedData(obj[key], depth + 1, maxDepth);
    }
    return result;
}

Why this matters: The maxDepth parameter limits recursion to 100 levels, preventing stack overflow. This limit is high enough for legitimate nested data structures (most real-world data rarely exceeds 10-20 levels) while being low enough to stop attacks before consuming significant stack memory. Malicious deeply nested input throws an error instead of crashing the application. The depth check occurs before processing, failing fast when limits are exceeded.

Conclusion

Add depth parameters to all recursive functions that process external data. Set reasonable maximum depths based on expected data structure complexity. Throw errors or return default values when depth limits are exceeded instead of crashing.

FAQs

Got Questions?

What's a reasonable maximum recursion depth?

Depends on your data structures. For JSON parsing or tree traversal, 100-1000 levels is reasonable. Most legitimate data structures don't exceed 10-20 levels. Set limits based on your domain, but always have limits. Monitor production to see actual depths and adjust accordingly.

How do I convert recursion to iteration?

Use explicit stacks or queues. Replace recursive calls with loop that pushes items onto a stack, then pops and processes them. This gives you full control over memory usage and depth. For tree traversal, breadth-first or depth-first iteration with explicit data structures prevents stack overflow.

Should I check depth at the start or end of recursive calls?

At the start, before any processing. This fails fast when limits are exceeded, preventing wasted computation on data that will be rejected. Guard clauses at function entry make depth checks explicit and easy to audit.

How do I handle cyclic references in recursive functions?

Track visited objects in a Set or WeakSet. Before recursing, check if the object was already visited. If yes, either skip it, throw an error, or return a placeholder. This prevents infinite recursion from circular data structures: obj.child.parent === obj.

Get secure for free

Secure your code, cloud, and runtime in one central system.
Find and fix vulnerabilities fast automatically.

No credit card required | Scan results in 32secs.