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:
- Service Availability: A single malicious request can crash the entire application, causing downtime
- Resource Exhaustion: Stack overflow errors can leave the process in an unstable state
- Cascading Failures: In containerized environments, repeated crashes can trigger restart loops
- 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:
pyproject.toml: Specifies the minimum safe version (>=3.11.6) so future installations get the fixuv.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.tomlanduv.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.