Always ask for confirmation when pasting text with control codes in it

This commit is contained in:
Kovid Goyal
2023-10-20 13:02:28 +05:30
parent 56963c693e
commit defa2e29ac
7 changed files with 32 additions and 19 deletions

View File

@@ -72,7 +72,7 @@ Detailed list of changes
- Two new event types for :ref:`watchers <watchers>`, :code:`on_title_change` and :code:`on_set_user_var` - Two new event types for :ref:`watchers <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] 0.30.1 [2023-10-05]

View File

@@ -523,7 +523,7 @@ clipboard.
''' '''
) )
opt('paste_actions', 'quote-urls-at-prompt', opt('paste_actions', 'quote-urls-at-prompt,confirm',
option_type='paste_actions', option_type='paste_actions',
long_text=''' long_text='''
A comma separated list of actions to take when pasting text into the terminal. A comma separated list of actions to take when pasting text into the terminal.
@@ -533,8 +533,12 @@ The supported paste actions are:
If the text being pasted is a URL and the cursor is at a shell prompt, If the text being pasted is a URL and the cursor is at a shell prompt,
automatically quote the URL (needs :opt:`shell_integration`). automatically quote the URL (needs :opt:`shell_integration`).
:code:`confirm`: :code:`confirm`:
Confirm the paste if bracketed paste mode is not active or there is Confirm the paste if the text to be pasted contains any terminal control codes
a large amount of text being pasted. 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`: :code:`filter`:
Run the filter_paste() function from the file :file:`paste-actions.py` in 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 the kitty config directory on the pasted text. The text returned by the

View File

@@ -544,7 +544,7 @@ class Options:
mark3_foreground: Color = Color(0, 0, 0) mark3_foreground: Color = Color(0, 0, 0)
mouse_hide_wait: float = 0.0 if is_macos else 3.0 mouse_hide_wait: float = 0.0 if is_macos else 3.0
open_url_with: typing.List[str] = ['default'] 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' placement_strategy: choices_for_placement_strategy = 'center'
pointer_shape_when_dragging: choices_for_pointer_shape_when_dragging = 'beam' pointer_shape_when_dragging: choices_for_pointer_shape_when_dragging = 'beam'
pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow' pointer_shape_when_grabbed: choices_for_pointer_shape_when_grabbed = 'arrow'

View File

@@ -911,7 +911,7 @@ def shell_integration(x: str) -> FrozenSet[str]:
def paste_actions(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(',')) q = frozenset(x.lower().split(','))
if not q.issubset(s): if not q.issubset(s):
log_error(f'Invalid paste actions: {q - s}, ignoring') log_error(f'Invalid paste actions: {q - s}, ignoring')

View File

@@ -1135,16 +1135,13 @@ def docs_url(which: str = '', local_docs_root: Optional[str] = '') -> str:
return url 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~') pat = re.compile(b'(?:(?:\033\\\x5b)|(?:\x9b))201~')
while True: while True:
new_text = pat.sub(b'', text) new_text = pat.sub(b'', text)
if new_text == text: if new_text == text:
break break
text = new_text 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 return text

View File

@@ -1468,18 +1468,31 @@ class Window:
text = shlex.quote(text) text = shlex.quote(text)
btext = text.encode('utf-8') btext = text.encode('utf-8')
if 'confirm' in opts.paste_actions: 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 = '' msg = ''
limit = 16 * 1024 if len(btext) > 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:
msg = _('Pasting very large amounts of text ({} bytes) can be slow.').format(len(btext)) 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_large_paste_confirmation, btext), window=self)
get_boss().confirm(msg + _(' Are you sure?'), partial(self.handle_paste_confirmation, btext), window=self)
return return
self.paste_text(btext) 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: if confirmed:
self.paste_text(btext) self.paste_text(btext)
@@ -1494,7 +1507,7 @@ class Window:
if isinstance(text, str): if isinstance(text, str):
text = text.encode('utf-8') text = text.encode('utf-8')
if self.screen.in_bracketed_paste_mode: if self.screen.in_bracketed_paste_mode:
text = sanitize_for_bracketed_paste(text, self.at_prompt) text = sanitize_for_bracketed_paste(text)
else: else:
# Workaround for broken editors like nano that cannot handle # Workaround for broken editors like nano that cannot handle
# newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994 # newlines in pasted text see https://github.com/kovidgoyal/kitty/issues/994

View File

@@ -590,7 +590,6 @@ class TestDataTypes(BaseTest):
self.assertNotIn(b'\x1b[201~', q) self.assertNotIn(b'\x1b[201~', q)
self.assertNotIn('\x9b201~'.encode('utf-8'), q) self.assertNotIn('\x9b201~'.encode('utf-8'), q)
self.assertIn(b'ab', 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): def test_expand_ansi_c_escapes(self):
for src, expected in { for src, expected in {