From defa2e29ac1eacd130fcffa300d9be8f6e76ddfc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 20 Oct 2023 13:02:28 +0530 Subject: [PATCH] Always ask for confirmation when pasting text with control codes in it --- docs/changelog.rst | 2 +- kitty/options/definition.py | 10 +++++++--- kitty/options/types.py | 2 +- kitty/options/utils.py | 2 +- kitty/utils.py | 5 +---- kitty/window.py | 29 +++++++++++++++++++++-------- kitty_tests/datatypes.py | 1 - 7 files changed, 32 insertions(+), 19 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 24ce7a200..2e8fb1277 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -72,7 +72,7 @@ Detailed list of changes - Two new event types for :ref:`watchers `, :code:`on_title_change` and :code:`on_set_user_var` -- When pasting in bracketed paste mode and the cursor is at a shell prompt, strip out C0 control codes as some shells incorrectly interpret these allowing escape from bracketed paste mode. Thanks to David Leadbetter for discovering. +- When pasting, if the text contains terminal control codes ask the user for permission. See :opt:`paste_actions` for details. Thanks to David Leadbetter for discovering this. 0.30.1 [2023-10-05] diff --git a/kitty/options/definition.py b/kitty/options/definition.py index 9a2ca804d..5cde3f63b 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -523,7 +523,7 @@ ''' ) -opt('paste_actions', 'quote-urls-at-prompt', +opt('paste_actions', 'quote-urls-at-prompt,confirm', option_type='paste_actions', long_text=''' A comma separated list of actions to take when pasting text into the terminal. @@ -533,8 +533,12 @@ If the text being pasted is a URL and the cursor is at a shell prompt, automatically quote the URL (needs :opt:`shell_integration`). :code:`confirm`: - Confirm the paste if bracketed paste mode is not active or there is - a large amount of text being pasted. + Confirm the paste if the text to be pasted contains any terminal control codes + as this can be dangerous, leading to code execution if the shell/program running + in the terminal does not properly handle these. +:code:`confirm-if-large` + Confirm the paste if it is very large (larger than 16KB) as pasting + large amounts of text into shells can be very slow. :code:`filter`: Run the filter_paste() function from the file :file:`paste-actions.py` in the kitty config directory on the pasted text. The text returned by the diff --git a/kitty/options/types.py b/kitty/options/types.py index 45d05aa82..f18f0d619 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -544,7 +544,7 @@ class Options: mark3_foreground: Color = Color(0, 0, 0) mouse_hide_wait: float = 0.0 if is_macos else 3.0 open_url_with: typing.List[str] = ['default'] - paste_actions: typing.FrozenSet[str] = frozenset({'quote-urls-at-prompt'}) + paste_actions: typing.FrozenSet[str] = frozenset({'confirm', 'quote-urls-at-prompt'}) placement_strategy: choices_for_placement_strategy = 'center' pointer_shape_when_dragging: choices_for_pointer_shape_when_dragging = 'beam' pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow' diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 8340babb8..d03d96486 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -911,7 +911,7 @@ def shell_integration(x: str) -> FrozenSet[str]: def paste_actions(x: str) -> FrozenSet[str]: - s = frozenset({'quote-urls-at-prompt', 'confirm', 'filter'}) + s = frozenset({'quote-urls-at-prompt', 'confirm', 'filter', 'confirm-if-large'}) q = frozenset(x.lower().split(',')) if not q.issubset(s): log_error(f'Invalid paste actions: {q - s}, ignoring') diff --git a/kitty/utils.py b/kitty/utils.py index 250712fc2..dbd4444b3 100644 --- a/kitty/utils.py +++ b/kitty/utils.py @@ -1135,16 +1135,13 @@ def docs_url(which: str = '', local_docs_root: Optional[str] = '') -> str: return url -def sanitize_for_bracketed_paste(text: bytes, at_shell_prompt: bool = False) -> bytes: +def sanitize_for_bracketed_paste(text: bytes) -> bytes: pat = re.compile(b'(?:(?:\033\\\x5b)|(?:\x9b))201~') while True: new_text = pat.sub(b'', text) if new_text == text: break text = new_text - if at_shell_prompt: - # some shells dont handle C0 control codes in pasted text correctly - text = re.sub(b'[\x00-\x1f]+', b'', text) return text diff --git a/kitty/window.py b/kitty/window.py index 32a8c37c8..5357c9daa 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1468,18 +1468,31 @@ def paste_with_actions(self, text: str) -> None: text = shlex.quote(text) btext = text.encode('utf-8') if 'confirm' in opts.paste_actions: + sanitized = sanitize_control_codes(text) + if sanitized != text: + msg = _('The text to be pasted contains terminal control codes. If the terminal program you are pasting into does not properly' + ' sanitize pasted text, this can lead to code execution vulnerabilities. How would you like to proceed?') + get_boss().choose( + msg, partial(self.handle_dangerous_paste_confirmation, btext, sanitized), + 's;green:Sanitize and paste', 'p;red:Paste anyway', 'c;yellow:Cancel', + window=self, default='s', + ) + return + if 'confirm-if-large' in opts.paste_actions: msg = '' - limit = 16 * 1024 - if not self.screen.in_bracketed_paste_mode: - msg = _('Pasting text into shells that do not support bracketed paste can be dangerous.') - elif len(btext) > limit: + if len(btext) > 16 * 1024: msg = _('Pasting very large amounts of text ({} bytes) can be slow.').format(len(btext)) - if msg: - get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_paste_confirmation, btext), window=self) + get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_large_paste_confirmation, btext), window=self) return self.paste_text(btext) - def handle_paste_confirmation(self, btext: bytes, confirmed: bool) -> None: + def handle_dangerous_paste_confirmation(self, btext: bytes, sanitized: str, choice: str) -> None: + if choice == 's': + self.paste_text(sanitized) + elif choice == 'p': + self.paste_text(btext) + + def handle_large_paste_confirmation(self, btext: bytes, confirmed: bool) -> None: if confirmed: self.paste_text(btext) @@ -1494,7 +1507,7 @@ def paste_text(self, text: Union[str, bytes]) -> None: if isinstance(text, str): text = text.encode('utf-8') if self.screen.in_bracketed_paste_mode: - text = sanitize_for_bracketed_paste(text, self.at_prompt) + text = sanitize_for_bracketed_paste(text) else: # Workaround for broken editors like nano that cannot handle # newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994 diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index cbb5e012e..a90420a5e 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -590,7 +590,6 @@ def test_bracketed_paste_sanitizer(self): self.assertNotIn(b'\x1b[201~', q) self.assertNotIn('\x9b201~'.encode('utf-8'), q) self.assertIn(b'ab', q) - self.assertNotIn(b'\x03', sanitize_for_bracketed_paste(b'hi\x03world', True)) def test_expand_ansi_c_escapes(self): for src, expected in {