From c6311ffdf9f255dfc9f38746e9ae5771d1b7936f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 4 May 2026 13:49:07 +0200 Subject: [PATCH 1/2] gh-149231: tomllib: Limit the number of parts in a key (GH-149233) Co-authored-by: Stan Ulbrych (cherry picked from commit bc7c102f3462a9f014f3ac2546acfb471b2a7eae) --- Lib/test/test_tomllib/test_misc.py | 13 +++++++++++++ Lib/tomllib/_parser.py | 12 ++++++++++++ .../2026-05-01-16-45-31.gh-issue-149231.x2nBEE.rst | 1 + 3 files changed, 26 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-01-16-45-31.gh-issue-149231.x2nBEE.rst diff --git a/Lib/test/test_tomllib/test_misc.py b/Lib/test/test_tomllib/test_misc.py index 118fde24d88521..85526ed8015542 100644 --- a/Lib/test/test_tomllib/test_misc.py +++ b/Lib/test/test_tomllib/test_misc.py @@ -117,6 +117,19 @@ def test_inline_table_recursion_limit(self): recursive_table_toml = nest_count * "key = {" + nest_count * "}" tomllib.loads(recursive_table_toml) + def test_key_recursion_limit(self): + nest_count = tomllib._parser.MAX_KEY_PARTS - 2 + nested_key_toml = "a." * nest_count + "a = 1" + tomllib.loads(nested_key_toml) + + nest_count = tomllib._parser.MAX_KEY_PARTS + 2 + nested_key_toml = "a." * nest_count + "a = 1" + with self.assertRaisesRegex( + RecursionError, + r"TOML key has more than the allowed [0-9]+ parts", + ): + tomllib.loads(nested_key_toml) + def test_types_import(self): """Test that `_types` module runs. diff --git a/Lib/tomllib/_parser.py b/Lib/tomllib/_parser.py index 3ee47aa9e0afba..4e51a6c8d83263 100644 --- a/Lib/tomllib/_parser.py +++ b/Lib/tomllib/_parser.py @@ -4,6 +4,7 @@ from __future__ import annotations +import sys from types import MappingProxyType from ._re import ( @@ -22,6 +23,13 @@ from ._types import Key, ParseFloat, Pos +# Pathologically excessive number of parts in a key runs into quadratic +# behavior (e.g. in Flags.is_). +# Even if keys aren't currently parsed using recursion, they name a +# recursive structure, so it makes sense to limit it using getrecursionlimit() +# and RecursionError. +MAX_KEY_PARTS: Final = sys.getrecursionlimit() + ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) # Neither of these sets include quotation mark or backslash. They are @@ -462,6 +470,10 @@ def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]: pos = skip_chars(src, pos, TOML_WS) pos, key_part = parse_key_part(src, pos) key += (key_part,) + if len(key) > MAX_KEY_PARTS: + raise RecursionError( + f"TOML key has more than the allowed {MAX_KEY_PARTS} parts" + ) pos = skip_chars(src, pos, TOML_WS) diff --git a/Misc/NEWS.d/next/Library/2026-05-01-16-45-31.gh-issue-149231.x2nBEE.rst b/Misc/NEWS.d/next/Library/2026-05-01-16-45-31.gh-issue-149231.x2nBEE.rst new file mode 100644 index 00000000000000..c265b54db8bed4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-01-16-45-31.gh-issue-149231.x2nBEE.rst @@ -0,0 +1 @@ +In :mod:`tomllib`, the number of parts in TOML keys is now limited. From 63215334ff75716c6f16baa00a4a0616640bb235 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 14 May 2026 19:06:55 +0200 Subject: [PATCH 2/2] Apply suggestion from @StanFromIreland Co-authored-by: Stan Ulbrych --- Lib/tomllib/_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tomllib/_parser.py b/Lib/tomllib/_parser.py index 4e51a6c8d83263..84e70495dd0d3a 100644 --- a/Lib/tomllib/_parser.py +++ b/Lib/tomllib/_parser.py @@ -19,7 +19,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Iterable - from typing import IO, Any + from typing import IO, Any, Final from ._types import Key, ParseFloat, Pos