From 85407eed2005de1a4529c02a94ebb8b1cdee1548 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 25 Mar 2026 00:46:32 +0100 Subject: [PATCH 1/2] gh-146333: Fix quadratic regex backtracking in configparser option parsing --- Lib/configparser.py | 20 +++++++++++++++++++ Lib/test/test_configparser.py | 20 +++++++++++++++++++ ...3-25-00-51-03.gh-issue-146333.LqdL__bn.rst | 3 +++ 3 files changed, 43 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-03-25-00-51-03.gh-issue-146333.LqdL__bn.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index d435a5c2fe0da2..b546605226d4c9 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -1145,6 +1145,26 @@ def _handle_option(self, st, line, fpname): # an option line? st.indent_level = st.cur_indent_level + # Fast path: if no delimiter is present, skip the regex to avoid + # quadratic backtracking (gh-146333). When allow_no_value is True, + # treat the whole line as an option name with no value. + if not any(d in line.clean for d in self._delimiters): + if self._allow_no_value: + st.optname = self.optionxform(line.clean.strip()) + if not st.optname: + st.errors.append(ParsingError(fpname, st.lineno, line)) + return + if (self._strict and + (st.sectname, st.optname) in st.elements_added): + raise DuplicateOptionError(st.sectname, st.optname, + fpname, st.lineno) + st.elements_added.add((st.sectname, st.optname)) + st.cursect[st.optname] = None + return + else: + st.errors.append(ParsingError(fpname, st.lineno, line)) + return + mo = self._optcre.match(line.clean) if not mo: # a non-fatal parsing error occurred. set up the diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 1bfb53ccbb1398..d7c4f19c1a5ef0 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2270,6 +2270,26 @@ def test_section_bracket_in_key(self): output.close() +class ReDoSTestCase(unittest.TestCase): + """Regression tests for quadratic regex backtracking (gh-146333).""" + + def test_option_regex_does_not_backtrack(self): + # A line with many spaces between non-delimiter characters + # should be parsed in linear time, not quadratic. + parser = configparser.RawConfigParser() + content = "[section]\n" + "x" + " " * 40000 + "y" + "\n" + # This should complete almost instantly. Before the fix, + # it would take over a minute due to catastrophic backtracking. + with self.assertRaises(configparser.ParsingError): + parser.read_string(content) + + def test_option_regex_no_value_does_not_backtrack(self): + parser = configparser.RawConfigParser(allow_no_value=True) + content = "[section]\n" + "x" + " " * 40000 + "y" + "\n" + parser.read_string(content) + self.assertTrue(parser.has_option("section", "x" + " " * 40000 + "y")) + + class MiscTestCase(unittest.TestCase): def test__all__(self): support.check__all__(self, configparser, not_exported={"Error"}) diff --git a/Misc/NEWS.d/next/Security/2026-03-25-00-51-03.gh-issue-146333.LqdL__bn.rst b/Misc/NEWS.d/next/Security/2026-03-25-00-51-03.gh-issue-146333.LqdL__bn.rst new file mode 100644 index 00000000000000..96d86ecc0a0fb3 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-03-25-00-51-03.gh-issue-146333.LqdL__bn.rst @@ -0,0 +1,3 @@ +Fix quadratic backtracking in :class:`configparser.RawConfigParser` option +parsing regexes (``OPTCRE`` and ``OPTCRE_NV``). A crafted configuration line +with many whitespace characters could cause excessive CPU usage. From ef1c148a545e9c4aafb2c197c8e63697929d1454 Mon Sep 17 00:00:00 2001 From: joshuaswanson Date: Wed, 25 Mar 2026 16:05:44 +0100 Subject: [PATCH 2/2] Use negative lookahead in option regex to prevent backtracking --- Lib/configparser.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index b546605226d4c9..e76647d339e913 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -613,7 +613,9 @@ class RawConfigParser(MutableMapping): \] # ] """ _OPT_TMPL = r""" - (?P