https://github.com/python/cpython/commit/25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e
commit: 25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e
branch: main
author: Jacob Austin Lincoln <[email protected]>
committer: jaraco <[email protected]>
date: 2025-02-23T11:06:33-05:00
summary:

gh-65697: Prevent configparser from writing keys it cannot properly read 
(#129270)

---------

Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>

files:
A Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst
M Doc/library/configparser.rst
M Lib/configparser.py
M Lib/test/test_configparser.py

diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst
index ac0f3fca3d72fd..bb109a9b742cb7 100644
--- a/Doc/library/configparser.rst
+++ b/Doc/library/configparser.rst
@@ -1244,6 +1244,10 @@ ConfigParser Objects
       *space_around_delimiters* is true, delimiters between
       keys and values are surrounded by spaces.
 
+      .. versionchanged:: 3.14
+         Raises InvalidWriteError if this would write a representation which 
cannot
+         be accurately parsed by a future :meth:`read` call from this parser.
+
    .. note::
 
       Comments in the original configuration file are not preserved when
@@ -1459,6 +1463,17 @@ Exceptions
 
     .. versionadded:: 3.14
 
+.. exception:: InvalidWriteError
+
+   Exception raised when an attempted :meth:`ConfigParser.write` would not be 
parsed
+   accurately with a future :meth:`ConfigParser.read` call.
+
+   Ex: Writing a key beginning with the :attr:`ConfigParser.SECTCRE` pattern
+   would parse as a section header when read. Attempting to write this will 
raise
+   this exception.
+
+   .. versionadded:: 3.14
+
 .. rubric:: Footnotes
 
 .. [1] Config parsers allow for heavy customization.  If you are interested in
diff --git a/Lib/configparser.py b/Lib/configparser.py
index 9ff52e03c2c458..462af2f4abf867 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -161,7 +161,7 @@
            "InterpolationMissingOptionError", "InterpolationSyntaxError",
            "ParsingError", "MissingSectionHeaderError",
            "MultilineContinuationError", "UnnamedSectionDisabledError",
-           "ConfigParser", "RawConfigParser",
+           "InvalidWriteError", "ConfigParser", "RawConfigParser",
            "Interpolation", "BasicInterpolation",  "ExtendedInterpolation",
            "SectionProxy", "ConverterMapping",
            "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION")
@@ -375,6 +375,14 @@ class _UnnamedSection:
     def __repr__(self):
         return "<UNNAMED_SECTION>"
 
+class InvalidWriteError(Error):
+    """Raised when attempting to write data that the parser would read back 
differently.
+    ex: writing a key which begins with the section header pattern would read 
back as a
+    new section """
+
+    def __init__(self, msg=''):
+        Error.__init__(self, msg)
+
 
 UNNAMED_SECTION = _UnnamedSection()
 
@@ -973,6 +981,7 @@ def _write_section(self, fp, section_name, section_items, 
delimiter, unnamed=Fal
         if not unnamed:
             fp.write("[{}]\n".format(section_name))
         for key, value in section_items:
+            self._validate_key_contents(key)
             value = self._interpolation.before_write(self, section_name, key,
                                                      value)
             if value is not None or not self._allow_no_value:
@@ -1210,6 +1219,14 @@ def _convert_to_boolean(self, value):
             raise ValueError('Not a boolean: %s' % value)
         return self.BOOLEAN_STATES[value.lower()]
 
+    def _validate_key_contents(self, key):
+        """Raises an InvalidWriteError for any keys containing
+        delimiters or that match the section header pattern"""
+        if re.match(self.SECTCRE, key):
+            raise InvalidWriteError("Cannot write keys matching section 
pattern")
+        if any(delim in key for delim in self._delimiters):
+            raise InvalidWriteError("Cannot write key that contains 
delimiters")
+
     def _validate_value_types(self, *, section="", option="", value=""):
         """Raises a TypeError for illegal non-string values.
 
diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py
index c2c82ebe6a87aa..1313ec2b9e884e 100644
--- a/Lib/test/test_configparser.py
+++ b/Lib/test/test_configparser.py
@@ -2192,6 +2192,30 @@ def test_multiple_configs(self):
         self.assertEqual('2', cfg[configparser.UNNAMED_SECTION]['b'])
 
 
+class InvalidInputTestCase(unittest.TestCase):
+    """Tests for issue #65697, where configparser will write configs
+    it parses back differently. Ex: keys containing delimiters or
+    matching the section pattern"""
+
+    def test_delimiter_in_key(self):
+        cfg = configparser.ConfigParser(delimiters=('='))
+        cfg.add_section('section1')
+        cfg.set('section1', 'a=b', 'c')
+        output = io.StringIO()
+        with self.assertRaises(configparser.InvalidWriteError):
+            cfg.write(output)
+        output.close()
+
+    def test_section_bracket_in_key(self):
+        cfg = configparser.ConfigParser()
+        cfg.add_section('section1')
+        cfg.set('section1', '[this parses back as a section]', 'foo')
+        output = io.StringIO()
+        with self.assertRaises(configparser.InvalidWriteError):
+            cfg.write(output)
+        output.close()
+
+
 class MiscTestCase(unittest.TestCase):
     def test__all__(self):
         support.check__all__(self, configparser, not_exported={"Error"})
diff --git 
a/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst 
b/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst
new file mode 100644
index 00000000000000..3d4883e20ed242
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst
@@ -0,0 +1 @@
+stdlib configparser will now attempt to validate that keys it writes will not 
result in file corruption (creating a file unable to be accurately parsed by a 
future read() call from the same parser). Attempting a corrupting write() will 
raise an InvalidWriteError.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to