CVE-2022-41322: Improper Encoding or Escaping of Output (CWE-116)
value: In Kitty before 0.26.2, insufficient validation in the desktop notification escape sequence can lead to arbitrary code execution. The user must display attacker-controlled content in the terminal, then click on a notification popup.
CWE description: The product prepares a structured message for communication with another component, but encoding or escaping of the data is either missing or done incorrectly. As a result, the intended structure of the message is not preserved.
CVSS v3 base score: 7.8
Published: 2022-09-23T05:15Z
Repository: https://github.com/kovidgoyal/kitty
Language: Python
Files changed: docs/changelog.rst, kitty/notify.py, kitty/window.py, kitty_tests/datatypes.py
Commit date: 2022-09-05 10:41:19 +0530
Commit message: Sanitize notifications ids as they are retransmitted over the TTY

Diff: diff --git a/docs/changelog.rst b/docs/changelog.rst
index 6c9851f2a..926770e19 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -54,10 +54,14 @@ Detailed list of changes
 
 - Fix a regression in 0.26.0 that broke mapping of native keys who key codes did not fit in 21 bits (:iss:`5452`)
 
 - Wayland: Fix remembering window size not accurate when client side decorations are present
 
+- Fix an issue where notification identifiers were not sanitized leading to
+  code execution if the user clicked on a notification popup from a malicious
+  source. Thanks to Carter Sande for discovering this vulnerability.
+
 0.26.1 [2022-08-30]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 - ssh kitten: Fix executable permission missing from kitty bootstrap script (:iss:`5438`)
 
diff --git a/kitty/notify.py b/kitty/notify.py
index 00dfff5a6..ad316c509 100644
--- a/kitty/notify.py
+++ b/kitty/notify.py
@@ -1,15 +1,17 @@
 #!/usr/bin/env python3
 # License: GPLv3 Copyright: 2019, Kovid Goyal <kovid at kovidgoyal.net>
 
+import re
 from base64 import standard_b64decode
 from collections import OrderedDict
 from itertools import count
-from typing import Dict, Optional, Callable
+from typing import Callable, Dict, Optional
 
 from .constants import is_macos, logo_png_file
 from .fast_data_types import get_boss
+from .types import run_once
 from .utils import log_error
 
 NotifyImplementation = Callable[[str, str, str], None]
 
 if is_macos:
@@ -90,10 +92,15 @@ def parse_osc_777(raw: str) -> NotificationCommand:
     if len(parts) > 1:
         ans.body = parts[1]
     return ans
 
 
+@run_once
+def sanitize_identifier_pat() -> 're.Pattern[str]':
+    return re.compile(r'[^a-zA-Z0-9-_+.]+')
+
+
 def parse_osc_99(raw: str) -> NotificationCommand:
     cmd = NotificationCommand()
     metadata, payload = raw.partition(';')[::2]
     payload_is_encoded = False
     payload_type = 'title'
@@ -105,11 +112,11 @@ def parse_osc_99(raw: str) -> NotificationCommand:
                 log_error('Malformed OSC 99: metadata is not key=value pairs')
                 return cmd
             if k == 'p':
                 payload_type = v
             elif k == 'i':
-                cmd.identifier = v
+                cmd.identifier = sanitize_identifier_pat().sub('', v)
             elif k == 'e':
                 payload_is_encoded = v == '1'
             elif k == 'd':
                 cmd.done = v != '0'
             elif k == 'a':
diff --git a/kitty/window.py b/kitty/window.py
index 544aae739..db92dc9cf 100644
--- a/kitty/window.py
+++ b/kitty/window.py
@@ -39,11 +39,11 @@
     set_clipboard_string, set_titlebar_color, set_window_logo,
     set_window_padding, set_window_render_data, update_ime_position_for_window,
     update_window_title, update_window_visibility, wakeup_main_loop
 )
 from .keys import keyboard_mode_name, mod_mask
-from .notify import NotificationCommand, handle_notification_cmd
+from .notify import NotificationCommand, handle_notification_cmd, sanitize_identifier_pat
 from .options.types import Options
 from .rgb import to_color
 from .terminfo import get_capabilities
 from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
 from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict
@@ -999,10 +999,11 @@ def report_color(self, code: str, r: int, g: int, b: int) -> None:
         g |= g << 8
         b |= b << 8
         self.screen.send_escape_code_to_child(OSC, f'{code};rgb:{r:04x}/{g:04x}/{b:04x}')
 
     def report_notification_activated(self, identifier: str) -> None:
+        identifier = sanitize_identifier_pat().sub('', identifier)
         self.screen.send_escape_code_to_child(OSC, f'99;i={identifier};')
 
     def set_dynamic_color(self, code: int, value: Union[str, bytes]) -> None:
         if isinstance(value, bytes):
             value = value.decode('utf-8')
diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py
index 9369a115f..f4622a134 100644
--- a/kitty_tests/datatypes.py
+++ b/kitty_tests/datatypes.py
@@ -553,5 +553,9 @@ def test_single_key(self):
         self.ae(repr(SingleKey(key=23)._replace(key=-1, mods=GLFW_MOD_KITTY)), f'SingleKey(mods={GLFW_MOD_KITTY})')
         self.assertEqual(SingleKey(key=1), SingleKey(key=1))
         self.assertEqual(hash(SingleKey(key=1)), hash(SingleKey(key=1)))
         self.assertNotEqual(hash(SingleKey(key=1, mods=2)), hash(SingleKey(key=1)))
         self.assertNotEqual(SingleKey(key=1, mods=2), SingleKey(key=1))
+
+    def test_notify_identifier_sanitization(self):
+        from kitty.notify import sanitize_identifier_pat
+        self.ae(sanitize_identifier_pat().sub('', '\x1b\nabc\n[*'), 'abc')

Vulnerable code: from typing import Dict, Optional, Callable
                cmd.identifier = v
from .notify import NotificationCommand, handle_notification_cmd
Fixed code: - Fix an issue where notification identifiers were not sanitized leading to
  code execution if the user clicked on a notification popup from a malicious
  source. Thanks to Carter Sande for discovering this vulnerability.

import re
from typing import Callable, Dict, Optional
from .types import run_once
@run_once
def sanitize_identifier_pat() -> 're.Pattern[str]':
    return re.compile(r'[^a-zA-Z0-9-_+.]+')


                cmd.identifier = sanitize_identifier_pat().sub('', v)
from .notify import NotificationCommand, handle_notification_cmd, sanitize_identifier_pat
        identifier = sanitize_identifier_pat().sub('', identifier)

    def test_notify_identifier_sanitization(self):
        from kitty.notify import sanitize_identifier_pat
        self.ae(sanitize_identifier_pat().sub('', '\x1b\nabc\n[*'), 'abc')