Back to Blog
high SEVERITY7 min read

How Denial of Service via Unbounded Recursion happens in Python JSON parsing and how to fix it

A high-severity denial of service vulnerability (CVE-2025-67221) was discovered in orjson 3.10.16, where deeply nested JSON documents could trigger unbounded recursion and crash the application. The fix upgrades orjson to version 3.11.6, which implements recursion depth limits to prevent stack exhaustion attacks.

O
By Orbis AppSec
Published August 22, 2026Reviewed August 22, 2026

Answer Summary

CVE-2025-67221 is a denial of service vulnerability in orjson (Python's fast JSON library) caused by unbounded recursion when parsing deeply nested JSON documents, mapped to CWE-674 (Uncontrolled Recursion). Attackers can craft malicious JSON payloads with hundreds or thousands of nested arrays/objects to exhaust the call stack and crash the application. The fix upgrades from orjson 3.10.16 to 3.11.6, which enforces recursion depth limits during JSON deserialization to prevent stack overflow attacks.

Vulnerability at a Glance

cweCWE-674 (Uncontrolled Recursion)
fixUpgrade orjson to 3.11.6 with built-in recursion guards
riskApplication crash from malicious JSON input
languagePython
root causeNo recursion depth limit when parsing nested JSON structures
vulnerabilityDenial of Service via Unbounded Recursion

Introduction

In the asn_coverage project, a high-severity vulnerability was discovered in the asn_coverage/uv.lock dependency file. The project was using orjson version 3.10.16, a high-performance JSON serialization library for Python. This version contained CVE-2025-67221, a denial of service vulnerability that allows attackers to crash the application by sending deeply nested JSON documents. The vulnerability affects any code path that processes JSON input from untrusted sources—a common pattern in web APIs, data processing pipelines, and configuration parsers.

The specific issue lies in how orjson 3.10.16 handles recursive descent during JSON deserialization. When parsing nested structures like {"a": {"b": {"c": {...}}}} or [[[[...]]]], the parser calls itself recursively for each level. Without a depth limit, an attacker can craft a payload with thousands of nesting levels that exhausts the Python call stack, triggering a RecursionError and crashing the application.

The Vulnerability Explained

CVE-2025-67221 is a classic example of uncontrolled recursion (CWE-674). Here's what makes this vulnerability dangerous:

The Vulnerable Code Pattern

In orjson 3.10.16, the JSON deserialization logic recursively processes nested structures without enforcing a maximum depth. While the exact implementation is in Rust (orjson's core is written in Rust for performance), the vulnerability manifests when Python code calls orjson.loads() on malicious input:

import orjson

# Vulnerable code using orjson 3.10.16
def process_json_data(json_string):
    # No depth validation before parsing
    data = orjson.loads(json_string)
    return data

The problem isn't in the application code—it's in the library itself. When orjson.loads() encounters deeply nested JSON, it recursively descends through each level:

orjson.loads()  parse_object()  parse_value()  parse_object()  parse_value()  ...

Attack Scenario

Consider the asn_coverage application, which likely processes JSON data related to ASN (Autonomous System Number) coverage information. An attacker could exploit this in several ways:

Scenario 1: API Endpoint Attack

# Attacker sends a POST request to /api/coverage
POST /api/coverage HTTP/1.1
Content-Type: application/json

{"data": {"level1": {"level2": {"level3": { ... 5000 more levels ... }}}}}

Scenario 2: Malicious Configuration File
If the application reads JSON configuration files, an attacker with write access could replace a config file with:

[[[[[[[[[[[[[[[[[[[[...2000 levels deep...]]]]]]]]]]]]]]]]]]]]

Scenario 3: Data Processing Pipeline
If asn_coverage processes JSON from external sources (S3 buckets, API responses, etc.), a compromised upstream service could inject malicious JSON:

# Application code in asn_coverage
import boto3
import orjson

s3 = boto3.client('s3')
response = s3.get_object(Bucket='asn-data', Key='coverage.json')
data = orjson.loads(response['Body'].read())  # Vulnerable!

Real-World Impact

The impact of this vulnerability is significant:

  1. Service Availability: A single malicious request can crash the entire application, causing downtime
  2. Resource Exhaustion: Stack overflow errors can leave the process in an unstable state
  3. Cascading Failures: In containerized environments, repeated crashes can trigger restart loops
  4. Security Monitoring Blind Spots: DoS attacks can mask other malicious activity

The vulnerability is particularly dangerous because:
- JSON is ubiquitous in modern applications
- The payload can be surprisingly small (a few KB can contain thousands of nesting levels)
- It requires no authentication—any endpoint accepting JSON is vulnerable
- It's trivial to exploit with automated tools

The Fix

The fix is straightforward but critical: upgrade orjson from version 3.10.16 to 3.11.6. Here's what changed:

Before (Vulnerable Code)

# asn_coverage/pyproject.toml
dependencies = [
    "arrow>=1.3.0",
    "boto3>=1.37.36",
    "click>=8.1.8",
    "orjson>=3.10.16",  # Vulnerable version
    "requests>=2.32.3",
]

After (Secure Code)

# asn_coverage/pyproject.toml
dependencies = [
    "arrow>=1.3.0",
    "boto3>=1.37.36",
    "click>=8.1.8",
    "orjson>=3.11.6",   # Fixed version with recursion limits
    "requests>=2.32.3",
]

Lock File Updates

The fix also updates the uv.lock file to ensure consistent dependency resolution:

 version = 1
-revision = 1
+revision = 3
 requires-python = ">=3.12"

The revision bump from 1 to 3 indicates that the dependency tree was recalculated to incorporate the security fix.

How This Change Solves the Problem

orjson 3.11.6 introduces recursion depth limits during JSON deserialization. The library now tracks nesting depth and rejects payloads that exceed safe thresholds (typically 1024 levels). This prevents stack exhaustion while still supporting legitimate use cases:

# With orjson 3.11.6
import orjson

# Legitimate JSON: works fine
data = orjson.loads('{"user": {"profile": {"settings": {"theme": "dark"}}}}')

# Malicious JSON: raises exception before stack overflow
malicious = '{"a":' * 5000 + '{}' + '}' * 5000
try:
    orjson.loads(malicious)
except orjson.JSONDecodeError as e:
    print(f"Rejected: {e}")  # "Rejected: recursion limit exceeded"

Why Both Files Changed

The fix modified two files for complete protection:

  1. pyproject.toml: Specifies the minimum safe version (>=3.11.6) so future installations get the fix
  2. uv.lock: Locks the exact resolved version to ensure reproducible builds and prevent accidental downgrades

This two-file approach is critical for Python projects using modern dependency management. Without updating the lock file, developers might still install the vulnerable version despite the pyproject.toml change.

Prevention & Best Practices

1. Implement Defense in Depth

Don't rely solely on library updates. Add application-level protections:

import orjson

MAX_JSON_SIZE = 1_000_000  # 1 MB
MAX_NESTING_DEPTH = 100

def safe_json_parse(json_bytes: bytes):
    # Size check
    if len(json_bytes) > MAX_JSON_SIZE:
        raise ValueError("JSON payload too large")

    # Parse with updated library
    data = orjson.loads(json_bytes)

    # Validate depth (recursive check)
    def check_depth(obj, depth=0):
        if depth > MAX_NESTING_DEPTH:
            raise ValueError("JSON nesting too deep")
        if isinstance(obj, dict):
            for value in obj.values():
                check_depth(value, depth + 1)
        elif isinstance(obj, list):
            for item in obj:
                check_depth(item, depth + 1)

    check_depth(data)
    return data

2. Use Dependency Scanning Tools

Integrate automated scanning into your CI/CD pipeline:

# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'

3. Monitor for Anomalous JSON Payloads

Implement runtime monitoring to detect attack attempts:

import logging
from functools import wraps

def monitor_json_parsing(func):
    @wraps(func)
    def wrapper(json_data, *args, **kwargs):
        try:
            result = func(json_data, *args, **kwargs)
            return result
        except RecursionError:
            logging.critical(
                f"Recursion error in JSON parsing - possible DoS attempt",
                extra={"payload_size": len(json_data)}
            )
            raise
    return wrapper

@monitor_json_parsing
def process_api_request(json_data):
    return orjson.loads(json_data)

4. Follow OWASP Guidelines

Implement controls from the OWASP API Security Top 10:

  • API4:2023 Unrestricted Resource Consumption: Set limits on request size, parsing time, and complexity
  • API8:2023 Security Misconfiguration: Keep dependencies updated with automated tools

5. Consider Alternative Parsers for Untrusted Input

For highly sensitive applications processing untrusted JSON, consider:

  • Streaming parsers: Process JSON incrementally without building full object trees
  • Schema validation: Use JSON Schema to reject invalid structures before parsing
  • Sandboxing: Parse untrusted JSON in isolated processes with resource limits
import jsonschema

# Define maximum depth via schema
schema = {
    "type": "object",
    "maxProperties": 100,
    "additionalProperties": {
        "type": ["string", "number", "boolean", "null"]
        # Nested objects not allowed
    }
}

def validate_and_parse(json_data):
    data = orjson.loads(json_data)
    jsonschema.validate(data, schema)
    return data

Key Takeaways

  • orjson 3.10.16 allows unbounded recursion when parsing deeply nested JSON, enabling trivial DoS attacks against any endpoint accepting JSON input
  • The asn_coverage project's dependency files (pyproject.toml and uv.lock) required synchronized updates to enforce the minimum safe version across all environments
  • Small payloads can cause big damage: A JSON document with 5000 nesting levels can be just a few kilobytes but will crash most applications
  • Library updates alone aren't enough: Implement application-level depth checks, size limits, and monitoring to detect exploitation attempts
  • Lock files are security-critical: Always commit and update lock files (uv.lock, poetry.lock, Pipfile.lock) when fixing dependency vulnerabilities to prevent version drift

How Orbis AppSec Detected This

  • Source: JSON input from external sources (API requests, S3 objects, configuration files) processed by the asn_coverage application
  • Sink: orjson.loads() calls in version 3.10.16, which lacks recursion depth limits during deserialization
  • Missing control: No maximum nesting depth validation before or during JSON parsing
  • CWE: CWE-674 (Uncontrolled Recursion)
  • Fix: Upgraded orjson dependency from 3.10.16 to 3.11.6, which enforces recursion limits and rejects excessively nested JSON documents

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

CVE-2025-67221 demonstrates how a seemingly simple operation—parsing JSON—can become a critical security vulnerability when recursion isn't properly bounded. The upgrade from orjson 3.10.16 to 3.11.6 is essential for any Python application processing JSON from untrusted sources. Beyond this specific fix, developers should adopt a defense-in-depth approach: validate input depth, monitor for anomalies, and regularly scan dependencies for known vulnerabilities.

The asn_coverage project's fix serves as a model for responsible dependency management: update both the dependency specification and lock file, document the security rationale, and verify that the change preserves expected behavior. By staying vigilant about dependency security and implementing multiple layers of protection, you can build resilient applications that withstand both known and emerging threats.

References

Frequently Asked Questions

What is unbounded recursion in JSON parsing?

Unbounded recursion occurs when a JSON parser recursively processes nested structures (arrays within arrays, objects within objects) without limiting the depth, allowing deeply nested input to exhaust the call stack and crash the application.

How do you prevent unbounded recursion DoS in Python?

Use JSON libraries with built-in recursion limits (like orjson 3.11.6+), validate input depth before parsing, implement timeouts for parsing operations, and consider using iterative parsers for untrusted input.

What CWE is unbounded recursion?

CWE-674 (Uncontrolled Recursion), which describes functions that call themselves or mutual recursion without proper termination conditions, leading to stack exhaustion.

Is input size validation enough to prevent recursion DoS?

No. A small JSON payload (even a few KB) can contain thousands of nested levels that cause stack overflow. You must limit nesting depth, not just payload size.

Can static analysis detect unbounded recursion vulnerabilities?

Yes. Tools like Trivy, Snyk, and dependency scanners can identify vulnerable library versions. SAST tools can also flag recursive functions without depth checks, though JSON parsing vulnerabilities are best caught through dependency scanning.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #319

Related Articles

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.

high

How Information Disclosure via Unstripped Credential Headers Happens in Electron Apps and How to Fix It

A high-severity vulnerability (CVE-2026-54673) in the builder-util-runtime package allowed sensitive credential headers to leak during HTTP redirects in Electron applications. The fix upgrades builder-util-runtime from version 9.5.1 to 9.7.0, which properly strips authentication headers before following redirects to prevent information disclosure.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

high

How Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot