diff options
author | Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com> | 2025-02-23 16:06:33 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-23 16:06:33 (GMT) |
commit | 25a7ddf2efeaf77bcf94dbfca28ba3a6fe9ab57e (patch) | |
tree | 1e4c59ae665d4f14e4e714ede90375a79a30383c | |
parent | 1e4a4344af4f5fdc7157b195c7d333d088540035 (diff) | |
download | cpython-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.rst | 15 | ||||
-rw-r--r-- | Lib/configparser.py | 19 | ||||
-rw-r--r-- | Lib/test/test_configparser.py | 24 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst | 1 |
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. |