diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index c76db20c58792d..2327cfbf9b8b77 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1203,10 +1203,7 @@ def load_extension(self, name): self.apply_bindings(keydefs) for vevent in keydefs: methodname = vevent.replace("-", "_") - while methodname[:1] == '<': - methodname = methodname[1:] - while methodname[-1:] == '>': - methodname = methodname[:-1] + methodname = methodname.lstrip('<').rstrip('>') methodname = methodname + "_event" if hasattr(ins, methodname): self.text.bind(vevent, getattr(ins, methodname)) @@ -1341,6 +1338,24 @@ def set_indentation_params(self, is_py_src, guess=True): self.usetabs = False self.set_tk_tabwidth(self.tabwidth) + def delete_trail_char_and_space(self, want, chars, tabwidth): + chars = chars[:-1] # remove last character unconditionally + current_pos = 0 + ncharsretained = 0 + for char in chars: + if char == '\t': + current_pos = (current_pos // tabwidth + 1) * tabwidth + else: + current_pos += 1 + if current_pos > want: + break + ncharsretained += 1 + for i in range(ncharsretained, len(chars)): + if chars[i] not in " \t": + ncharsretained = i + 1 + chars = chars[:ncharsretained] + return chars + def smart_backspace_event(self, event): text = self.text first, last = self.get_selection_indices() @@ -1369,13 +1384,10 @@ def smart_backspace_event(self, event): assert have > 0 want = ((have - 1) // self.indentwidth) * self.indentwidth # Debug prompt is multilined.... - ncharsdeleted = 0 - while True: - chars = chars[:-1] - ncharsdeleted = ncharsdeleted + 1 - have = len(chars.expandtabs(tabwidth)) - if have <= want or chars[-1] not in " \t": - break + oldchars = chars + chars = self.delete_trail_char_and_space(want, chars, tabwidth) + ncharsdeleted = len(oldchars) - len(chars) + have = len(chars.expandtabs(tabwidth)) text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 0dfe2f3c58befa..b5ecf36d2eb345 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -1,10 +1,11 @@ -"Test editor, coverage 53%." +"Test editor, coverage approximately 53%." from idlelib import editor import unittest from collections import namedtuple from test.support import requires from tkinter import Tk, Text +import time Editor = editor.EditorWindow @@ -237,5 +238,64 @@ def test_rclick(self): pass +class DeleteWantTest(unittest.TestCase): + + data = [ + ("abcde" + 10000 * "\t" + 10000 * " ", [ + (30000, 4, "abcde" + 7499 * "\t"), + (41005, 4, "abcde" + 10000 * "\t" + 1001 * " "), + (3, 4, "abcde"), + (6, 4, "abcde"), + (30002, 4, "abcde" + 7499 * "\t"), + ]), + ("abcde\tabd\t\t", [ + (7, 4, "abcde\tabd"), + (12, 4, "abcde\tabd\t"), + (13, 4, "abcde\tabd\t"), + (16, 4, "abcde\tabd\t"), + ]), + ("abcde\tabd\t \ta", [ + (7, 4, "abcde\tabd"), + (12, 4, "abcde\tabd\t"), + (13, 4, "abcde\tabd\t "), + (16, 4, "abcde\tabd\t \t"), + ]), + (" ", [ + (2, 2, " "), + ]), + ] + + def mock_delete_trail_char_and_space(self, want, chars, tabwidth): + ncharsdeleted = 0 + while True: + chars = chars[:-1] + ncharsdeleted = ncharsdeleted + 1 + have = len(chars.expandtabs(tabwidth)) + if have <= want or chars[-1] not in " \t": + break + return chars + + def do_tests(self): + ew = Editor() + for dat in self.data: + test_str = dat[0] + for da in dat[1]: + with self.subTest(want=da[0], tabwidth=da[1], input=test_str): + res = ew.delete_trail_char_and_space(da[0], test_str, da[1]) + self.assertEqual(res, da[2]) + + def test_delete_trail_char_and_space(self): + with unittest.mock.patch.object(Editor, '__init__', return_value=None): + initial_time_new = time.time() + self.do_tests() + time_new = time.time() - initial_time_new + + with unittest.mock.patch.object(Editor, 'delete_trail_char_and_space', self.mock_delete_trail_char_and_space): + initial_time_old = time.time() + self.do_tests() + time_old = time.time() - initial_time_old + + self.assertGreaterEqual(time_old / time_new, 10) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst new file mode 100644 index 00000000000000..389b8af30ef048 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-05-29-03-24-18.gh-issue-134873.dziqkQ.rst @@ -0,0 +1 @@ +Fix a DOS vulnerability in :mod:`idlelib` regarding string slicing.