summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com>2025-02-23 16:06:33 (GMT)
committerGitHub <noreply@github.com>2025-02-23 16:06:33 (GMT)
commit25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e (patch)
tree1e4c59ae665d4f14e4e714ede90375a79a30383c
parent1e4a4344af4f5fdc7157b195c7d333d088540035 (diff)
downloadcpython-25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e.zip
cpython-25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e.tar.gz
cpython-25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e.tar.bz2
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>
-rw-r--r--Doc/library/configparser.rst15
-rw-r--r--Lib/configparser.py19
-rw-r--r--Lib/test/test_configparser.py24
-rw-r--r--Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst1
4 files changed, 58 insertions, 1 deletions
diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst
index ac0f3fc..bb109a9 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 9ff52e0..462af2f 100644
--- a/Lib/configparser.py
+++ b/Lib/configparser.py
@@ -161,7 +161,7 @@ __all__ = ("NoSectionError", "DuplicateOptionError", "DuplicateSectionError",
"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 @@ class RawConfigParser(MutableMapping):
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 @@ class RawConfigParser(MutableMapping):
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 c2c82eb..1313ec2 100644
--- a/Lib/test/test_configparser.py
+++ b/Lib/test/test_configparser.py
@@ -2192,6 +2192,30 @@ class SectionlessTestCase(unittest.TestCase):
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 0000000..3d4883e
--- /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.