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
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
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`
- 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]

View File

@ -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

View File

@ -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'

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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 {