Skip to content

Commit 25a7ddf

Browse files
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>
1 parent 1e4a434 commit 25a7ddf

File tree

4 files changed

+58
-1
lines changed

4 files changed

+58
-1
lines changed

Doc/library/configparser.rst

+15
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,10 @@ ConfigParser Objects
12441244
*space_around_delimiters* is true, delimiters between
12451245
keys and values are surrounded by spaces.
12461246

1247+
.. versionchanged:: 3.14
1248+
Raises InvalidWriteError if this would write a representation which cannot
1249+
be accurately parsed by a future :meth:`read` call from this parser.
1250+
12471251
.. note::
12481252

12491253
Comments in the original configuration file are not preserved when
@@ -1459,6 +1463,17 @@ Exceptions
14591463

14601464
.. versionadded:: 3.14
14611465

1466+
.. exception:: InvalidWriteError
1467+
1468+
Exception raised when an attempted :meth:`ConfigParser.write` would not be parsed
1469+
accurately with a future :meth:`ConfigParser.read` call.
1470+
1471+
Ex: Writing a key beginning with the :attr:`ConfigParser.SECTCRE` pattern
1472+
would parse as a section header when read. Attempting to write this will raise
1473+
this exception.
1474+
1475+
.. versionadded:: 3.14
1476+
14621477
.. rubric:: Footnotes
14631478

14641479
.. [1] Config parsers allow for heavy customization. If you are interested in

Lib/configparser.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
"InterpolationMissingOptionError", "InterpolationSyntaxError",
162162
"ParsingError", "MissingSectionHeaderError",
163163
"MultilineContinuationError", "UnnamedSectionDisabledError",
164-
"ConfigParser", "RawConfigParser",
164+
"InvalidWriteError", "ConfigParser", "RawConfigParser",
165165
"Interpolation", "BasicInterpolation", "ExtendedInterpolation",
166166
"SectionProxy", "ConverterMapping",
167167
"DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION")
@@ -375,6 +375,14 @@ class _UnnamedSection:
375375
def __repr__(self):
376376
return "<UNNAMED_SECTION>"
377377

378+
class InvalidWriteError(Error):
379+
"""Raised when attempting to write data that the parser would read back differently.
380+
ex: writing a key which begins with the section header pattern would read back as a
381+
new section """
382+
383+
def __init__(self, msg=''):
384+
Error.__init__(self, msg)
385+
378386

379387
UNNAMED_SECTION = _UnnamedSection()
380388

@@ -973,6 +981,7 @@ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=Fal
973981
if not unnamed:
974982
fp.write("[{}]\n".format(section_name))
975983
for key, value in section_items:
984+
self._validate_key_contents(key)
976985
value = self._interpolation.before_write(self, section_name, key,
977986
value)
978987
if value is not None or not self._allow_no_value:
@@ -1210,6 +1219,14 @@ def _convert_to_boolean(self, value):
12101219
raise ValueError('Not a boolean: %s' % value)
12111220
return self.BOOLEAN_STATES[value.lower()]
12121221

1222+
def _validate_key_contents(self, key):
1223+
"""Raises an InvalidWriteError for any keys containing
1224+
delimiters or that match the section header pattern"""
1225+
if re.match(self.SECTCRE, key):
1226+
raise InvalidWriteError("Cannot write keys matching section pattern")
1227+
if any(delim in key for delim in self._delimiters):
1228+
raise InvalidWriteError("Cannot write key that contains delimiters")
1229+
12131230
def _validate_value_types(self, *, section="", option="", value=""):
12141231
"""Raises a TypeError for illegal non-string values.
12151232

Lib/test/test_configparser.py

+24
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,30 @@ def test_multiple_configs(self):
21922192
self.assertEqual('2', cfg[configparser.UNNAMED_SECTION]['b'])
21932193

21942194

2195+
class InvalidInputTestCase(unittest.TestCase):
2196+
"""Tests for issue #65697, where configparser will write configs
2197+
it parses back differently. Ex: keys containing delimiters or
2198+
matching the section pattern"""
2199+
2200+
def test_delimiter_in_key(self):
2201+
cfg = configparser.ConfigParser(delimiters=('='))
2202+
cfg.add_section('section1')
2203+
cfg.set('section1', 'a=b', 'c')
2204+
output = io.StringIO()
2205+
with self.assertRaises(configparser.InvalidWriteError):
2206+
cfg.write(output)
2207+
output.close()
2208+
2209+
def test_section_bracket_in_key(self):
2210+
cfg = configparser.ConfigParser()
2211+
cfg.add_section('section1')
2212+
cfg.set('section1', '[this parses back as a section]', 'foo')
2213+
output = io.StringIO()
2214+
with self.assertRaises(configparser.InvalidWriteError):
2215+
cfg.write(output)
2216+
output.close()
2217+
2218+
21952219
class MiscTestCase(unittest.TestCase):
21962220
def test__all__(self):
21972221
support.check__all__(self, configparser, not_exported={"Error"})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)