Skip to content

Commit 6136b84

Browse files
spartan8806claude
andcommitted
Fix uncontrolled recursion DoS in parser.py (CWE-674)
Adds a configurable maximum nesting depth (default 100) to prevent RecursionError when parsing deeply nested arrays or inline tables. When the limit is exceeded, a ParseError is raised instead of allowing unbounded recursion that crashes the process. Fixes #459 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8e32f9c commit 6136b84

File tree

2 files changed

+92
-2
lines changed

2 files changed

+92
-2
lines changed

tests/test_recursion_fix.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Test for recursion depth fix in tomlkit parser.
3+
Verifies that deeply nested input raises ParseError instead of RecursionError.
4+
"""
5+
import sys
6+
sys.path.insert(0, ".") # Use local patched version if available
7+
8+
import tomlkit
9+
from tomlkit.exceptions import TOMLKitError
10+
11+
def test_deeply_nested_arrays():
12+
"""Deeply nested arrays should raise ParseError, not RecursionError."""
13+
payload = "x = " + "[" * 500 + "1" + "]" * 500
14+
try:
15+
tomlkit.parse(payload)
16+
print("[FAIL] No exception raised for 500-deep nested arrays")
17+
except RecursionError:
18+
print("[FAIL] RecursionError — fix not applied")
19+
except TOMLKitError as e:
20+
print(f"[PASS] ParseError raised: {e}")
21+
except Exception as e:
22+
print(f"[????] Unexpected: {type(e).__name__}: {e}")
23+
24+
def test_deeply_nested_inline_tables():
25+
"""Deeply nested inline tables should raise ParseError, not RecursionError."""
26+
payload = "x = " + "{a = " * 200 + "1" + "}" * 200
27+
try:
28+
tomlkit.parse(payload)
29+
print("[FAIL] No exception raised for 200-deep nested inline tables")
30+
except RecursionError:
31+
print("[FAIL] RecursionError — fix not applied")
32+
except TOMLKitError as e:
33+
print(f"[PASS] ParseError raised: {e}")
34+
except Exception as e:
35+
print(f"[????] Unexpected: {type(e).__name__}: {e}")
36+
37+
def test_normal_nesting_still_works():
38+
"""Reasonable nesting depth should still parse fine."""
39+
# 10 levels deep — well within limit
40+
payload = "x = " + "[" * 10 + "1" + "]" * 10
41+
try:
42+
doc = tomlkit.parse(payload)
43+
print(f"[PASS] 10-deep arrays parse OK")
44+
except Exception as e:
45+
print(f"[FAIL] Normal nesting broken: {e}")
46+
47+
# 5 levels inline tables
48+
payload2 = 'x = {a = {b = {c = {d = {e = 1}}}}}'
49+
try:
50+
doc2 = tomlkit.parse(payload2)
51+
print(f"[PASS] 5-deep inline tables parse OK")
52+
except Exception as e:
53+
print(f"[FAIL] Normal inline tables broken: {e}")
54+
55+
def test_mixed_nesting():
56+
"""Mixed arrays and inline tables at depth."""
57+
payload = "x = " + "[{a = " * 50 + "1" + "}]" * 50
58+
try:
59+
tomlkit.parse(payload)
60+
print("[PASS] 50-deep mixed nesting parsed (within default limit)")
61+
except RecursionError:
62+
print("[FAIL] RecursionError on mixed nesting")
63+
except TOMLKitError as e:
64+
print(f"[PASS] ParseError on mixed nesting: {e}")
65+
66+
if __name__ == "__main__":
67+
print("=== tomlkit recursion depth fix tests ===\n")
68+
test_normal_nesting_still_works()
69+
test_deeply_nested_arrays()
70+
test_deeply_nested_inline_tables()
71+
test_mixed_nesting()
72+
print("\nDone")

tomlkit/parser.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,16 @@ class Parser:
6363
Parser for TOML documents.
6464
"""
6565

66-
def __init__(self, string: str | bytes) -> None:
66+
# Default maximum nesting depth for arrays/inline tables
67+
DEFAULT_MAX_NESTING_DEPTH = 100
68+
69+
def __init__(self, string: str | bytes, max_nesting_depth: int | None = None) -> None:
6770
# Input to parse
6871
self._src = Source(decode(string))
6972

7073
self._aot_stack: list[Key] = []
74+
self._nesting_depth = 0
75+
self._max_nesting_depth = max_nesting_depth if max_nesting_depth is not None else self.DEFAULT_MAX_NESTING_DEPTH
7176

7277
@property
7378
def _state(self) -> _StateHandler:
@@ -582,6 +587,11 @@ def _parse_bool(self, style: BoolType) -> Bool:
582587
def _parse_array(self) -> Array:
583588
# Consume opening bracket, EOF here is an issue (middle of array)
584589
self.inc(exception=UnexpectedEofError)
590+
self._nesting_depth += 1
591+
if self._nesting_depth > self._max_nesting_depth:
592+
raise self.parse_error(
593+
InternalParserError, "Maximum nesting depth exceeded"
594+
)
585595

586596
elems: list[Item] = []
587597
prev_value = None
@@ -637,15 +647,22 @@ def _parse_array(self) -> Array:
637647
try:
638648
res = Array(elems, Trivia())
639649
except ValueError:
640-
pass
650+
self._nesting_depth -= 1
651+
raise
641652
else:
653+
self._nesting_depth -= 1
642654
return res
643655

644656
raise self.parse_error(ParseError, "Failed to parse array")
645657

646658
def _parse_inline_table(self) -> InlineTable:
647659
# consume opening bracket, EOF here is an issue (middle of array)
648660
self.inc(exception=UnexpectedEofError)
661+
self._nesting_depth += 1
662+
if self._nesting_depth > self._max_nesting_depth:
663+
raise self.parse_error(
664+
InternalParserError, "Maximum nesting depth exceeded"
665+
)
649666

650667
elems = Container(True)
651668
expect_key = True
@@ -685,6 +702,7 @@ def _parse_inline_table(self) -> InlineTable:
685702
self.inc(exception=UnexpectedEofError)
686703
expect_key = True
687704

705+
self._nesting_depth -= 1
688706
return InlineTable(elems, Trivia())
689707

690708
def _parse_number(self, raw: str, trivia: Trivia) -> Item | None:

0 commit comments

Comments
 (0)