The popular JSON parsing and serialization library, orjson, used widely in Python projects, has a Uncontrolled Recursion vulnerability in versions before 3.9.15. This vulnerability allows an attacker to crash applications using orjson through a denial-of-service (DoS) attack.
https://www.cve.org/CVERecord?id=CVE-2024-27454
CVSS Base Score: 7.5
CVSS v3.1 Vector:
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
All versions of orjson before 3.9.15 are affected by this vulnerability.
Upgrade to orjson 3.9.15 or later. The updated versions raise a
JSONDecodeError
when the input is too deeply nested,
preventing the uncontrolled recursion.
To highlight how different JSON packages handle recursive input, a simple test was conducted. This test incrementally increased input depth until an error occurred.
The behavior of orjson was compared against other popular choices. These include the Python standard library json, ujson (ultrajson), rapidjson, simplejson, and msgspec.
import json
from collections.abc import Callable
import msgspec
import orjson
import rapidjson
import simplejson
import ujson
from humanize import naturalsize
from tabulate import tabulate
def check_parsing(fn: Callable) -> tuple[int, Exception]:
= 0
input_size
try:
while True:
+= 2
input_size = input_size // 2
input_half_size = b'[' * input_half_size + b']' * input_half_size
input_bytes
fn(input_bytes)except Exception as e:
return input_size, e
def check_serialization(fn: Callable) -> tuple[int, Exception]:
= 2
input_size = []
input_data
try:
while True:
+= 2
input_size = [input_data]
input_data
fn(input_data)except Exception as e:
return input_size, e
= ('Package Name', 'Package Version', 'Input Size', 'Exception Type')
header = []
parsing_data = []
serialization_data
# json-like packages
for module in (json, orjson, ujson, rapidjson, simplejson):
= check_parsing(module.loads)
input_size, e = naturalsize(input_size, binary=True)
human_size __name__, module.__version__, human_size, type(e).__name__))
parsing_data.append((module.
= check_serialization(module.dumps)
input_size, e = naturalsize(input_size, binary=True)
human_size __name__, module.__version__, human_size, type(e).__name__))
serialization_data.append((module.
# msgspec
= check_parsing(msgspec.json.decode)
input_size, e = naturalsize(input_size, binary=True)
human_size __name__, msgspec.__version__, human_size, type(e).__name__))
parsing_data.append((msgspec.
= check_serialization(msgspec.json.encode)
input_size, e = naturalsize(input_size, binary=True)
human_size __name__, msgspec.__version__, human_size, type(e).__name__))
serialization_data.append((msgspec.
# summary
print('# Parsing behavior', end='\n\n')
print(tabulate(parsing_data, headers=header, tablefmt='github'), end='\n\n')
print('# Serialization behavior', end='\n\n')
print(tabulate(serialization_data, headers=header, tablefmt='github'))
Package Name | Package Version | Input Size | Exception Type |
---|---|---|---|
json | 2.0.9 | 2.9 KiB | RecursionError |
orjson | 3.9.15 | 2.0 KiB | JSONDecodeError |
ujson | 5.9.0 | 2.0 KiB | JSONDecodeError |
rapidjson | 1.16 | 2.0 KiB | RecursionError |
simplejson | 3.19.2 | 2.9 KiB | RecursionError |
msgspec | 0.18.6 | 2.9 KiB | RecursionError |
Package Name | Package Version | Input Size | Exception Type |
---|---|---|---|
json | 2.0.9 | 2.9 KiB | RecursionError |
orjson | 3.9.15 | 512 Bytes | TypeError |
ujson | 5.9.0 | 2.0 KiB | OverflowError |
rapidjson | 1.16 | 2.9 KiB | RecursionError |
simplejson | 3.19.2 | 2.9 KiB | RecursionError |
msgspec | 0.18.6 | 2.9 KiB | RecursionError |
Credit for discovering this vulnerability goes to David Buchanan. Thanks also go to Kamil Monicz for their analysis of the security implications of this issue.
If you find any inaccuracies or have suggestions to improve this blog post, please feel free to contact the author https://monicz.dev/#get-in-touch.