From a82ea8fb0338f2bd46cf627c4b763094448e6bd7 Mon Sep 17 00:00:00 2001 From: cdcadman Date: Wed, 17 May 2023 03:57:08 -0700 Subject: [PATCH 1/7] Address CVE-2023-26112 ReDoS --- src/configobj/validate.py | 2 +- src/tests/test_validate_errors.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/configobj/validate.py b/src/configobj/validate.py index 9267a3f..98d879f 100644 --- a/src/configobj/validate.py +++ b/src/configobj/validate.py @@ -541,7 +541,7 @@ class Validator(object): """ # this regex does the initial parsing of the checks - _func_re = re.compile(r'(.+?)\((.*)\)', re.DOTALL) + _func_re = re.compile(r'([^\(\)]+?)\((.*)\)', re.DOTALL) # this regex takes apart keyword arguments _key_arg = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$', re.DOTALL) diff --git a/src/tests/test_validate_errors.py b/src/tests/test_validate_errors.py index 399daa8..f7d6c27 100644 --- a/src/tests/test_validate_errors.py +++ b/src/tests/test_validate_errors.py @@ -3,7 +3,7 @@ import pytest from configobj import ConfigObj, get_extra_values, ParseError, NestingError -from configobj.validate import Validator +from configobj.validate import Validator, VdtUnknownCheckError @pytest.fixture() def thisdir(): @@ -77,3 +77,11 @@ def test_no_parent(tmpdir, specpath): ini.write('[[haha]]') with pytest.raises(NestingError): conf = ConfigObj(str(ini), configspec=specpath, file_error=True) + + +def test_re_dos(val): + value = "aaa" + i = 165100 + attack = '\x00'*i + ')' + '('*i + with pytest.raises(VdtUnknownCheckError): + val.check(attack, value) From 148bf8dd2ca25991719235d187dadee4d99930d5 Mon Sep 17 00:00:00 2001 From: Yegor Yefremov Date: Wed, 11 Oct 2023 09:38:40 +0200 Subject: [PATCH 2/7] setup.py: fix license tag The license is BSD-3-Clause and not BSD-2-Clause. Also use the related SPDX name. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f18451c..ce87e5f 100644 --- a/setup.py +++ b/setup.py @@ -124,8 +124,8 @@ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', classifiers=CLASSIFIERS, keywords=KEYWORDS, - license='BSD (2 clause)', + license='BSD-3-Clause', ) if __name__ == '__main__': - setup(**project) \ No newline at end of file + setup(**project) From 861383cd11dec0c2427724f0993437a1762c2a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sun, 11 Jun 2023 16:21:05 +0100 Subject: [PATCH 3/7] Drop support for Python < 3.5 --- .github/workflows/python-test.yml | 6 +-- README.md | 1 - setup.py | 11 +---- src/configobj/__init__.py | 53 +++++++++------------ src/configobj/validate.py | 76 ++++++++++--------------------- src/tests/configobj_doctests.py | 22 ++------- src/tests/test_configobj.py | 50 ++++++++------------ 7 files changed, 76 insertions(+), 143 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 2caa6ff..6bcbf41 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest coverage pytest-cov six mock + pip install pytest coverage pytest-cov if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install -e . - name: Test with pytest @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "2.7", "3.5", "3.6" ] + python-version: [ "3.5", "3.6" ] steps: - uses: actions/checkout@v3 @@ -46,7 +46,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest coverage pytest-cov six mock + pip install pytest coverage pytest-cov if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install -e . - name: Test with pytest diff --git a/README.md b/README.md index e55cf52..41314d9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ Python 3+ compatible port of the [configobj](https://pypi.python.org/pypi/configobj/) library. The Github CI/CD Pipeline runs tests on python versions: -- 2.7 - 3.5 - 3.6 - 3.7 diff --git a/setup.py b/setup.py index f18451c..ec3f422 100644 --- a/setup.py +++ b/setup.py @@ -40,10 +40,6 @@ DESCRIPTION = 'Config file reading, writing and validation.' URL = 'https://github.com/DiffSK/configobj' -REQUIRES = """ - six -""" - VERSION = '' with closing(open(os.path.join(__here__, 'src', PACKAGES[0], '_version.py'), 'r')) as handle: for line in handle.readlines(): @@ -88,8 +84,6 @@ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', @@ -120,12 +114,11 @@ py_modules=MODULES, package_dir={'': 'src'}, packages=PACKAGES, - install_requires=[i.strip() for i in REQUIRES.splitlines() if i.strip()], - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires='>=3.5', classifiers=CLASSIFIERS, keywords=KEYWORDS, license='BSD (2 clause)', ) if __name__ == '__main__': - setup(**project) \ No newline at end of file + setup(**project) diff --git a/src/configobj/__init__.py b/src/configobj/__init__.py index 0d752bc..3f91127 100644 --- a/src/configobj/__init__.py +++ b/src/configobj/__init__.py @@ -19,7 +19,6 @@ from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE -import six from ._version import __version__ # imported lazily to avoid startup performance hit if it isn't used @@ -121,10 +120,6 @@ def match_utf8(encoding): 'write_empty_values': False, } -# this could be replaced if six is used for compatibility, or there are no -# more assertions about items being a string - - def getObj(s): global compiler if compiler is None: @@ -553,11 +548,11 @@ def __getitem__(self, key): """Fetch the item and do string interpolation.""" val = dict.__getitem__(self, key) if self.main.interpolation: - if isinstance(val, six.string_types): + if isinstance(val, str): return self._interpolate(key, val) if isinstance(val, list): def _check(entry): - if isinstance(entry, six.string_types): + if isinstance(entry, str): return self._interpolate(key, entry) return entry new = [_check(entry) for entry in val] @@ -580,7 +575,7 @@ def __setitem__(self, key, value, unrepr=False): ``unrepr`` must be set when setting a value to a dictionary, without creating a new sub-section. """ - if not isinstance(key, six.string_types): + if not isinstance(key, str): raise ValueError('The key "%s" is not a string.' % key) # add the comment @@ -614,11 +609,11 @@ def __setitem__(self, key, value, unrepr=False): if key not in self: self.scalars.append(key) if not self.main.stringify: - if isinstance(value, six.string_types): + if isinstance(value, str): pass elif isinstance(value, (list, tuple)): for entry in value: - if not isinstance(entry, six.string_types): + if not isinstance(entry, str): raise TypeError('Value is not a string "%s".' % entry) else: raise TypeError('Value is not a string "%s".' % value) @@ -959,7 +954,7 @@ def as_bool(self, key): return False else: try: - if not isinstance(val, six.string_types): + if not isinstance(val, str): # TODO: Why do we raise a KeyError here? raise KeyError() else: @@ -1230,7 +1225,7 @@ def __init__(self, infile=None, options=None, configspec=None, encoding=None, def _load(self, infile, configspec): - if isinstance(infile, six.string_types): + if isinstance(infile, str): self.filename = infile if os.path.isfile(infile): with open(infile, 'rb') as h: @@ -1298,7 +1293,7 @@ def set_section(in_section, this_section): break break - assert all(isinstance(line, six.string_types) for line in content), repr(content) + assert all(isinstance(line, str) for line in content), repr(content) content = [line.rstrip('\r\n') for line in content] self._parse(content) @@ -1403,7 +1398,7 @@ def _handle_bom(self, infile): else: line = infile - if isinstance(line, six.text_type): + if isinstance(line, str): # it's already decoded and there's no need to do anything # else, just use the _decode utility method to handle # listifying appropriately @@ -1448,7 +1443,7 @@ def _handle_bom(self, infile): # No encoding specified - so we need to check for UTF8/UTF16 for BOM, (encoding, final_encoding) in list(BOMS.items()): - if not isinstance(line, six.binary_type) or not line.startswith(BOM): + if not isinstance(line, bytes) or not line.startswith(BOM): # didn't specify a BOM, or it's not a bytestring continue else: @@ -1464,9 +1459,9 @@ def _handle_bom(self, infile): else: infile = newline # UTF-8 - if isinstance(infile, six.text_type): + if isinstance(infile, str): return infile.splitlines(True) - elif isinstance(infile, six.binary_type): + elif isinstance(infile, bytes): return infile.decode('utf-8').splitlines(True) else: return self._decode(infile, 'utf-8') @@ -1474,12 +1469,8 @@ def _handle_bom(self, infile): return self._decode(infile, encoding) - if six.PY2 and isinstance(line, str): - # don't actually do any decoding, since we're on python 2 and - # returning a bytestring is fine - return self._decode(infile, None) # No BOM discovered and no encoding specified, default to UTF-8 - if isinstance(infile, six.binary_type): + if isinstance(infile, bytes): return infile.decode('utf-8').splitlines(True) else: return self._decode(infile, 'utf-8') @@ -1487,7 +1478,7 @@ def _handle_bom(self, infile): def _a_to_u(self, aString): """Decode ASCII strings to unicode if a self.encoding is specified.""" - if isinstance(aString, six.binary_type) and self.encoding: + if isinstance(aString, bytes) and self.encoding: return aString.decode(self.encoding) else: return aString @@ -1499,9 +1490,9 @@ def _decode(self, infile, encoding): if is a string, it also needs converting to a list. """ - if isinstance(infile, six.string_types): + if isinstance(infile, str): return infile.splitlines(True) - if isinstance(infile, six.binary_type): + if isinstance(infile, bytes): # NOTE: Could raise a ``UnicodeDecodeError`` if encoding: return infile.decode(encoding).splitlines(True) @@ -1510,7 +1501,7 @@ def _decode(self, infile, encoding): if encoding: for i, line in enumerate(infile): - if isinstance(line, six.binary_type): + if isinstance(line, bytes): # NOTE: The isinstance test here handles mixed lists of unicode/string # NOTE: But the decode will break on any non-string values # NOTE: Or could raise a ``UnicodeDecodeError`` @@ -1520,7 +1511,7 @@ def _decode(self, infile, encoding): def _decode_element(self, line): """Decode element to unicode if necessary.""" - if isinstance(line, six.binary_type) and self.default_encoding: + if isinstance(line, bytes) and self.default_encoding: return line.decode(self.default_encoding) else: return line @@ -1532,7 +1523,7 @@ def _str(self, value): Used by ``stringify`` within validate, to turn non-string values into strings. """ - if not isinstance(value, six.string_types): + if not isinstance(value, str): # intentially 'str' because it's just whatever the "normal" # string type is for the python version we're dealing with return str(value) @@ -1786,7 +1777,7 @@ def _quote(self, value, multiline=True): return self._quote(value[0], multiline=False) + ',' return ', '.join([self._quote(val, multiline=False) for val in value]) - if not isinstance(value, six.string_types): + if not isinstance(value, str): if self.stringify: # intentially 'str' because it's just whatever the "normal" # string type is for the python version we're dealing with @@ -2111,7 +2102,7 @@ def write(self, outfile=None, section=None): if not output.endswith(newline): output += newline - if isinstance(output, six.binary_type): + if isinstance(output, bytes): output_bytes = output else: output_bytes = output.encode(self.encoding or @@ -2353,7 +2344,7 @@ def reload(self): This method raises a ``ReloadError`` if the ConfigObj doesn't have a filename attribute pointing to a file. """ - if not isinstance(self.filename, six.string_types): + if not isinstance(self.filename, str): raise ReloadError() filename = self.filename diff --git a/src/configobj/validate.py b/src/configobj/validate.py index 98d879f..5eb7ab4 100644 --- a/src/configobj/validate.py +++ b/src/configobj/validate.py @@ -165,21 +165,6 @@ import sys from pprint import pprint -#TODO - #21 - six is part of the repo now, but we didn't switch over to it here -# this could be replaced if six is used for compatibility, or there are no -# more assertions about items being a string -if sys.version_info < (3,): - string_type = basestring -else: - string_type = str - # so tests that care about unicode on 2.x can specify unicode, and the same - # tests when run on 3.x won't complain about a undefined name "unicode" - # since all strings are unicode on 3.x we just want to pass it through - # unchanged - unicode = lambda x: x - # in python 3, all ints are equivalent to python 2 longs, and they'll - # never show "L" in the repr - long = int _list_arg = re.compile(r''' (?: @@ -258,17 +243,6 @@ _matchstring = '^%s*' % _paramstring -# Python pre 2.2.1 doesn't have bool -try: - bool -except NameError: - def bool(val): - """Simple boolean equivalent function. """ - if val: - return 1 - else: - return 0 - def dottedQuadToNum(ip): """ @@ -304,20 +278,20 @@ def numToDottedQuad(num): """ Convert int or long int to dotted quad string - >>> numToDottedQuad(long(-1)) + >>> numToDottedQuad(int(-1)) Traceback (most recent call last): ValueError: Not a good numeric IP: -1 - >>> numToDottedQuad(long(1)) + >>> numToDottedQuad(int(1)) '0.0.0.1' - >>> numToDottedQuad(long(16777218)) + >>> numToDottedQuad(int(16777218)) '1.0.0.2' - >>> numToDottedQuad(long(16908291)) + >>> numToDottedQuad(int(16908291)) '1.2.0.3' - >>> numToDottedQuad(long(16909060)) + >>> numToDottedQuad(int(16909060)) '1.2.3.4' - >>> numToDottedQuad(long(4294967295)) + >>> numToDottedQuad(int(4294967295)) '255.255.255.255' - >>> numToDottedQuad(long(4294967296)) + >>> numToDottedQuad(int(4294967296)) Traceback (most recent call last): ValueError: Not a good numeric IP: 4294967296 >>> numToDottedQuad(-1) @@ -343,11 +317,11 @@ def numToDottedQuad(num): import socket, struct # no need to intercept here, 4294967295L is fine - if num > long(4294967295) or num < 0: + if num > int(4294967295) or num < 0: raise ValueError('Not a good numeric IP: %s' % num) try: return socket.inet_ntoa( - struct.pack('!L', long(num))) + struct.pack('!L', int(num))) except (socket.error, struct.error, OverflowError): raise ValueError('Not a good numeric IP: %s' % num) @@ -491,9 +465,9 @@ class Validator(object): ... # check that value is of the correct type. ... # possible valid inputs are integers or strings ... # that represent integers - ... if not isinstance(value, (int, long, string_type)): + ... if not isinstance(value, (int, str)): ... raise VdtTypeError(value) - ... elif isinstance(value, string_type): + ... elif isinstance(value, str): ... # if we are given a string ... # attempt to convert to an integer ... try: @@ -763,7 +737,7 @@ def _is_num_param(names, values, to_float=False): for (name, val) in zip(names, values): if val is None: out_params.append(val) - elif isinstance(val, (int, long, float, string_type)): + elif isinstance(val, (int, float, str)): try: out_params.append(fun(val)) except ValueError as e: @@ -781,7 +755,7 @@ def _is_num_param(names, values, to_float=False): def is_integer(value, min=None, max=None): """ - A check that tests that a given value is an integer (int, or long) + A check that tests that a given value is an integer (int) and optionally, between bounds. A negative value is accepted, while a float will fail. @@ -820,9 +794,9 @@ def is_integer(value, min=None, max=None): 0 """ (min_val, max_val) = _is_num_param(('min', 'max'), (min, max)) - if not isinstance(value, (int, long, string_type)): + if not isinstance(value, (int, str)): raise VdtTypeError(value) - if isinstance(value, string_type): + if isinstance(value, str): # if it's a string - does it represent an integer ? try: value = int(value) @@ -872,7 +846,7 @@ def is_float(value, min=None, max=None): """ (min_val, max_val) = _is_num_param( ('min', 'max'), (min, max), to_float=True) - if not isinstance(value, (int, long, float, string_type)): + if not isinstance(value, (int, float, str)): raise VdtTypeError(value) if not isinstance(value, float): # if it's a string - does it represent a float ? @@ -937,7 +911,7 @@ def is_boolean(value): VdtTypeError: the value "up" is of the wrong type. """ - if isinstance(value, string_type): + if isinstance(value, str): try: return bool_dict[value.lower()] except KeyError: @@ -980,7 +954,7 @@ def is_ip_addr(value): Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. """ - if not isinstance(value, string_type): + if not isinstance(value, str): raise VdtTypeError(value) value = value.strip() try: @@ -1022,7 +996,7 @@ def is_list(value, min=None, max=None): VdtTypeError: the value "12" is of the wrong type. """ (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) - if isinstance(value, string_type): + if isinstance(value, str): raise VdtTypeError(value) try: num_members = len(value) @@ -1091,7 +1065,7 @@ def is_string(value, min=None, max=None): Traceback (most recent call last): VdtValueTooLongError: the value "1234" is too long. """ - if not isinstance(value, string_type): + if not isinstance(value, str): raise VdtTypeError(value) (min_len, max_len) = _is_num_param(('min', 'max'), (min, max)) try: @@ -1197,7 +1171,7 @@ def is_string_list(value, min=None, max=None): Traceback (most recent call last): VdtTypeError: the value "hello" is of the wrong type. """ - if isinstance(value, string_type): + if isinstance(value, str): raise VdtTypeError(value) return [is_string(mem) for mem in is_list(value, min, max)] @@ -1325,7 +1299,7 @@ def is_option(value, *options): Traceback (most recent call last): VdtTypeError: the value "0" is of the wrong type. """ - if not isinstance(value, string_type): + if not isinstance(value, str): raise VdtTypeError(value) if not value in options: raise VdtValueError(value) @@ -1399,13 +1373,13 @@ def _test(value, *args, **keywargs): Bug test for unicode arguments >>> v = Validator() - >>> v.check(unicode('string(min=4)'), unicode('test')) == unicode('test') + >>> v.check('string(min=4)', 'test') == 'test' True >>> v = Validator() - >>> v.get_default_value(unicode('string(min=4, default="1234")')) == unicode('1234') + >>> v.get_default_value('string(min=4, default="1234")') == '1234' True - >>> v.check(unicode('string(min=4, default="1234")'), unicode('test')) == unicode('test') + >>> v.check('string(min=4, default="1234")', 'test') == 'test' True >>> v = Validator() diff --git a/src/tests/configobj_doctests.py b/src/tests/configobj_doctests.py index ab9e04d..5a44d49 100644 --- a/src/tests/configobj_doctests.py +++ b/src/tests/configobj_doctests.py @@ -15,25 +15,13 @@ # https://github.com/DiffSK/configobj import sys -# StringIO is used to simulate config files during doctests -if sys.version_info >= (3,): - # Python 3.x case (io does exist in 2.7, but better to use the 2.x case): - #http://bugs.python.org/issue8025 - from io import StringIO -else: - # Python 2.x case, explicitly NOT using cStringIO due to unicode edge cases - from StringIO import StringIO - -import os -import sys -INTP_VER = sys.version_info[:2] -if INTP_VER < (2, 2): - raise RuntimeError("Python v.2.2 or later needed") -from codecs import BOM_UTF8 +from io import StringIO + +import sys from configobj import * -from configobj.validate import Validator, VdtValueTooSmallError +from configobj.validate import Validator def _test_validate(): @@ -969,7 +957,7 @@ def _test_validation_with_preserve_errors(): a = ConfigObj(testconfig1.split('\n'), raise_errors=True) b = ConfigObj(testconfig2.split(b'\n'), raise_errors=True) i = ConfigObj(testconfig6.split(b'\n'), raise_errors=True) - globs.update({'INTP_VER': INTP_VER, 'a': a, 'b': b, 'i': i}) + globs.update({'a': a, 'b': b, 'i': i}) pre_failures, pre_tests = doctest.testmod( m, globs=globs, optionflags=doctest.IGNORE_EXCEPTION_DETAIL | doctest.ELLIPSIS) diff --git a/src/tests/test_configobj.py b/src/tests/test_configobj.py index b9004fd..017428a 100644 --- a/src/tests/test_configobj.py +++ b/src/tests/test_configobj.py @@ -8,7 +8,7 @@ from tempfile import NamedTemporaryFile import pytest -import six +import io import configobj as co from configobj import ConfigObj, flatten_errors, ReloadError, DuplicateError, MissingInterpolationOption, InterpolationLoopError, ConfigObjError @@ -36,19 +36,19 @@ def cfg_lines(config_string_representation): '{!r}'.format(config_string_representation)) first_content = lines[line_no_with_content] - if isinstance(first_content, six.binary_type): + if isinstance(first_content, bytes): first_content = first_content.decode('utf-8') - ws_chars = len(re.search('^(\s*)', first_content).group(1)) + ws_chars = len(re.search(r'^(\s*)', first_content).group(1)) def yield_stringified_line(): for line in lines: - if isinstance(line, six.binary_type): + if isinstance(line, bytes): yield line.decode('utf-8') else: yield line - return [re.sub('^\s{0,%s}' % ws_chars, '', line).encode('utf-8') + return [re.sub(r'^\s{0,%s}' % ws_chars, '', line).encode('utf-8') for line in yield_stringified_line()] @@ -70,7 +70,7 @@ def make_file_with_contents_and_return_name(config_string_representation): with NamedTemporaryFile(delete=False, mode='wb') as cfg_file: for line in lines: - if isinstance(line, six.binary_type): + if isinstance(line, bytes): cfg_file.write(line + os.linesep.encode('utf-8')) else: cfg_file.write((line + os.linesep).encode('utf-8')) @@ -186,11 +186,7 @@ def test_unicode_conversion_when_encoding_is_set(self, cfg_contents): c = ConfigObj(cfg, encoding='utf8') - if six.PY2: - assert not isinstance(c['test'], str) - assert isinstance(c['test'], unicode) - else: - assert isinstance(c['test'], str) + assert isinstance(c['test'], str) #issue #18 @@ -198,11 +194,7 @@ def test_no_unicode_conversion_when_encoding_is_omitted(self, cfg_contents): cfg = cfg_contents(b"test = some string") c = ConfigObj(cfg) - if six.PY2: - assert isinstance(c['test'], str) - assert not isinstance(c['test'], unicode) - else: - assert isinstance(c['test'], str) + assert isinstance(c['test'], str) #issue #44 def test_that_encoding_using_list_of_strings(self): @@ -210,11 +202,7 @@ def test_that_encoding_using_list_of_strings(self): c = ConfigObj(cfg, encoding='utf8') - if six.PY2: - assert isinstance(c['test'], unicode) - assert not isinstance(c['test'], str) - else: - assert isinstance(c['test'], str) + assert isinstance(c['test'], str) assert c['test'] == '\U0001f41c' @@ -223,7 +211,7 @@ def test_encoding_in_subsections(self, ant_cfg, cfg_contents): c = cfg_contents(ant_cfg) cfg = ConfigObj(c, encoding='utf-8') - assert isinstance(cfg['tags']['bug']['translated'], six.text_type) + assert isinstance(cfg['tags']['bug']['translated'], str) #issue #44 and #55 def test_encoding_in_config_files(self, request, ant_cfg): @@ -233,7 +221,7 @@ def test_encoding_in_config_files(self, request, ant_cfg): request.addfinalizer(lambda : os.unlink(cfg_file.name)) cfg = ConfigObj(cfg_file.name, encoding='utf-8') - assert isinstance(cfg['tags']['bug']['translated'], six.text_type) + assert isinstance(cfg['tags']['bug']['translated'], str) cfg.write() @pytest.fixture @@ -500,7 +488,7 @@ def test_unicode_handling(): 'section': {'test': 'test', 'test2': 'test2'}} uc = ConfigObj(u, encoding='utf_8', default_encoding='latin-1') assert uc.BOM - assert isinstance(uc['test1'], six.text_type) + assert isinstance(uc['test1'], str) assert uc.encoding == 'utf_8' assert uc.newlines == '\n' assert len(uc.write()) == 13 @@ -508,14 +496,14 @@ def test_unicode_handling(): a_list = uc.write() assert 'latin1' in str(a_list) assert len(a_list) == 14 - assert isinstance(a_list[0], six.binary_type) + assert isinstance(a_list[0], bytes) assert a_list[0].startswith(BOM_UTF8) u = u_base.replace('\n', '\r\n').encode('utf-8').splitlines(True) uc = ConfigObj(u) assert uc.newlines == '\r\n' uc.newlines = '\r' - file_like = six.BytesIO() + file_like = io.BytesIO() uc.write(file_like) file_like.seek(0) uc2 = ConfigObj(file_like) @@ -723,7 +711,7 @@ def transform(section, key): val = section[key] newkey = key.replace('XXXX', 'CLIENT1') section.rename(key, newkey) - if isinstance(val, six.string_types): + if isinstance(val, str): val = val.replace('XXXX', 'CLIENT1') section[newkey] = val @@ -811,7 +799,7 @@ def reloadable_cfg_content(self): return content def test_handle_no_filename(self): - for bad_args in ([six.BytesIO()], [], [[]]): + for bad_args in ([io.BytesIO()], [], [[]]): cfg = ConfigObj(*bad_args) with pytest.raises(ReloadError) as excinfo: cfg.reload() @@ -1264,21 +1252,21 @@ class TestEdgeCasesWhenWritingOut(object): def test_newline_terminated(self, empty_cfg): empty_cfg.newlines = '\n' empty_cfg['a'] = 'b' - collector = six.BytesIO() + collector = io.BytesIO() empty_cfg.write(collector) assert collector.getvalue() == b'a = b\n' def test_hash_escaping(self, empty_cfg): empty_cfg.newlines = '\n' empty_cfg['#a'] = 'b # something' - collector = six.BytesIO() + collector = io.BytesIO() empty_cfg.write(collector) assert collector.getvalue() == b'"#a" = "b # something"\n' empty_cfg = ConfigObj() empty_cfg.newlines = '\n' empty_cfg['a'] = 'b # something', 'c # something' - collector = six.BytesIO() + collector = io.BytesIO() empty_cfg.write(collector) assert collector.getvalue() == b'a = "b # something", "c # something"\n' From 008165c8f200a7c636c9095868c8c540f104eeda Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Tue, 17 Sep 2024 11:08:17 +0000 Subject: [PATCH 4/7] Drop python 3.5 from GitHub action, since it now fails to download --- .github/workflows/python-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 6bcbf41..ca5b99d 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.5", "3.6" ] + python-version: [ "3.6" ] steps: - uses: actions/checkout@v3 From fdf3634418d9acfc72a534b26a796d195c4a7e42 Mon Sep 17 00:00:00 2001 From: Jelmer Vernooij Date: Tue, 17 Sep 2024 11:24:01 +0000 Subject: [PATCH 5/7] Drop extra '2014' in LICENSE file. Fixes #233 --- LICENSE | 1 - 1 file changed, 1 deletion(-) diff --git a/LICENSE b/LICENSE index dddf422..c223322 100644 --- a/LICENSE +++ b/LICENSE @@ -13,7 +13,6 @@ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -2014 * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. From d6f7597df5ff4f91081c0d9329db1d8cbc0990f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 21 Sep 2024 12:39:06 +0000 Subject: [PATCH 6/7] Update minimum python to 3.7 everywhere, and add 3.12 --- .github/workflows/python-test.yml | 26 -------------------------- README.md | 3 +-- setup.py | 5 ++--- 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index ca5b99d..32b1adf 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -28,29 +28,3 @@ jobs: python src/tests/configobj_doctests.py python -m configobj.validate py.test -c setup.cfg --color=yes --cov=configobj --cov-report=term --cov-report=html --cov-report=xml - - - build-on-legacy: - runs-on: ubuntu-20.04 - strategy: - fail-fast: false - matrix: - python-version: [ "3.6" ] - - steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest coverage pytest-cov - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install -e . - - name: Test with pytest - run: | - python src/tests/configobj_doctests.py - python -m configobj.validate - py.test -c setup.cfg --color=yes --cov=configobj --cov-report=term --cov-report=html --cov-report=xml diff --git a/README.md b/README.md index 41314d9..5b25e70 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,12 @@ Python 3+ compatible port of the [configobj](https://pypi.python.org/pypi/configobj/) library. The Github CI/CD Pipeline runs tests on python versions: -- 3.5 -- 3.6 - 3.7 - 3.8 - 3.9 - 3.10 - 3.11 +- 3.12 ## Documentation diff --git a/setup.py b/setup.py index 71e70b1..9af51c9 100644 --- a/setup.py +++ b/setup.py @@ -85,13 +85,12 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Operating System :: OS Independent', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Python Modules', @@ -114,7 +113,7 @@ py_modules=MODULES, package_dir={'': 'src'}, packages=PACKAGES, - python_requires='>=3.5', + python_requires='>=3.7', classifiers=CLASSIFIERS, keywords=KEYWORDS, license='BSD-3-Clause', From 242dfd09510e937574758da76ddc644b71f8cc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Sat, 21 Sep 2024 12:46:51 +0000 Subject: [PATCH 7/7] release 5.0.9 --- CHANGES.rst | 8 +++++++- docs/configobj.rst | 6 ++++++ setup.py | 10 +++------- src/configobj/_version.py | 2 +- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a39a1d4..16462c3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,12 @@ Changelog --------- +Release 5.0.9 +""""""""""""" + +* drop support for Python 2 and <3.7 +* fix CVE-2023-26112, ReDoS attack + Release 5.0.8 """"""""""""" @@ -31,4 +37,4 @@ Older Releases * Release 4.7.2 fixes several bugs in 4.7.1 * Release 4.7.1 fixes a bug with the deprecated options keyword in 4.7.0. * Release 4.7.0 improves performance adds features for validation and - fixes some bugs. \ No newline at end of file + fixes some bugs. diff --git a/docs/configobj.rst b/docs/configobj.rst index 1463bd7..ef6fe28 100644 --- a/docs/configobj.rst +++ b/docs/configobj.rst @@ -2383,6 +2383,12 @@ CHANGELOG This is an abbreviated changelog showing the major releases up to version 4. From version 4 it lists all releases and changes. +2024/09/21 - Version 5.0.9 +-------------------------- + +* drop support for Python 2 and <3.7 +* fix CVE-2023-26112, ReDoS attack + 2023/01/18 - Version 5.0.8 -------------------------- diff --git a/setup.py b/setup.py index 9af51c9..dbb87e9 100644 --- a/setup.py +++ b/setup.py @@ -23,13 +23,9 @@ from setuptools import setup -if sys.version_info < (2, 6): - print('for Python versions < 2.6 use configobj ' - 'version 4.7.2') - sys.exit(1) -elif sys.version_info < (2, 7): - print('for Python version 2.6 use configobj ' - 'version 5.0.6') +if sys.version_info[0] < 2: + print('for Python versions < 3 use configobj ' + 'version 5.0.8') sys.exit(1) __here__ = os.path.abspath(os.path.dirname(__file__)) diff --git a/src/configobj/_version.py b/src/configobj/_version.py index f9d71a5..5f25180 100644 --- a/src/configobj/_version.py +++ b/src/configobj/_version.py @@ -1 +1 @@ -__version__ = '5.0.8' \ No newline at end of file +__version__ = '5.0.9'