mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-21 04:32:05 +00:00
ssh kitten: Verify owner and permissions of SHM memory used for askpass
Some checks are pending
CI / Linux (python=3.14 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.13 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run
Some checks are pending
CI / Linux (python=3.14 cc=clang sanitize=1) (push) Waiting to run
CI / Linux (python=3.12 cc=gcc sanitize=0) (push) Waiting to run
CI / Linux (python=3.13 cc=gcc sanitize=1) (push) Waiting to run
CI / Linux package (push) Waiting to run
CI / Bundle test (macos-latest) (push) Waiting to run
CI / Bundle test (ubuntu-latest) (push) Waiting to run
CI / macOS Brew (push) Waiting to run
CI / Test ./dev.sh and benchmark (push) Waiting to run
CodeQL / CodeQL-Build (actions, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, macos-latest) (push) Waiting to run
CodeQL / CodeQL-Build (c, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (go, ubuntu-latest) (push) Waiting to run
CodeQL / CodeQL-Build (python, ubuntu-latest) (push) Waiting to run
Depscan / Scan dependencies for vulnerabilities (push) Waiting to run
This commit is contained in:
parent
41095672e8
commit
3281919056
5 changed files with 59 additions and 18 deletions
|
|
@ -281,6 +281,8 @@ Detailed list of changes
|
|||
|
||||
- Text sizing protocol: Fix a buffer overflow when a natural width text sizing escape code contains a grapheme cluster longer than four codepoints (:cve:`2026-80431`)
|
||||
|
||||
- ssh kitten askpass: Verify owner and permissions of SHM memory used for askpass
|
||||
|
||||
- Graphics protocol: Fix a crash when transmitting image data via a file or
|
||||
shared memory object (``t=f``, ``t=t`` or ``t=s``) and the client truncates
|
||||
it while kitty is reading from it.
|
||||
|
|
|
|||
|
|
@ -159,17 +159,12 @@ def create_shared_memory(data: Any, prefix: str) -> str:
|
|||
|
||||
def read_data_from_shared_memory(shm_name: str) -> Any:
|
||||
import json
|
||||
import stat
|
||||
|
||||
from kitty.shm import SharedMemory
|
||||
|
||||
with SharedMemory(shm_name, readonly=True) as shm:
|
||||
shm.unlink()
|
||||
if shm.stats.st_uid != os.geteuid() or shm.stats.st_gid != os.getegid():
|
||||
raise ValueError(f'Incorrect owner on pwfile: uid={shm.stats.st_uid} gid={shm.stats.st_gid}')
|
||||
mode = stat.S_IMODE(shm.stats.st_mode)
|
||||
if mode != stat.S_IREAD | stat.S_IWRITE:
|
||||
raise ValueError(f'Incorrect permissions on pwfile: 0o{mode:03o}')
|
||||
shm.verify_owner_and_mode()
|
||||
return json.loads(shm.read_data_with_size())
|
||||
|
||||
|
||||
|
|
|
|||
10
kitty/shm.py
10
kitty/shm.py
|
|
@ -160,6 +160,16 @@ class SharedMemory:
|
|||
def fileno(self) -> int:
|
||||
return self._fd
|
||||
|
||||
def verify_owner_and_mode(self) -> None:
|
||||
"""Ensure the shared memory object is owned by us and not accessible to
|
||||
any other user. Must be called when opening objects whose name comes
|
||||
from an untrusted source, as anyone can create an object of that name."""
|
||||
if self.stats.st_uid != os.geteuid() or self.stats.st_gid != os.getegid():
|
||||
raise ValueError(f'Incorrect owner on shared memory object: uid={self.stats.st_uid} gid={self.stats.st_gid}')
|
||||
mode = stat.S_IMODE(self.stats.st_mode)
|
||||
if mode != stat.S_IREAD | stat.S_IWRITE:
|
||||
raise ValueError(f'Incorrect permissions on shared memory object: 0o{mode:03o}')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{self.__class__.__name__}({self.name!r}, size={self.size})'
|
||||
|
||||
|
|
|
|||
|
|
@ -1808,23 +1808,40 @@ class Window:
|
|||
def handle_remote_askpass(self, msgb: memoryview) -> None:
|
||||
from .shm import SharedMemory
|
||||
|
||||
msg = str(msgb, 'utf-8')
|
||||
with SharedMemory(name=msg, readonly=True) as shm:
|
||||
name = str(msgb, 'utf-8')
|
||||
try:
|
||||
shm = SharedMemory(name=name)
|
||||
except OSError as err:
|
||||
log_error(f'Ignoring ask request as opening its shared memory object failed with error: {err}')
|
||||
return
|
||||
try:
|
||||
# The askpass client polls its own mapping for the answer, so
|
||||
# unlinking the name here does not affect it and prevents anyone
|
||||
# else from opening this object while we are using it.
|
||||
shm.unlink()
|
||||
shm.verify_owner_and_mode()
|
||||
shm.seek(1)
|
||||
data = json.loads(shm.read_data_with_size())
|
||||
q = str(data['type'])
|
||||
message: str = sanitize_control_codes(data['message'])
|
||||
choices = tuple(map(sanitize_control_codes, data['choices'])) if q == 'choose' else ()
|
||||
except Exception as err:
|
||||
shm.close()
|
||||
log_error(f'Ignoring invalid ask request with error: {err}')
|
||||
return
|
||||
|
||||
def callback(ans: Any) -> None:
|
||||
data = json.dumps(ans)
|
||||
with SharedMemory(name=msg) as shm:
|
||||
try:
|
||||
shm.seek(1)
|
||||
shm.write_data_with_size(data)
|
||||
shm.write_data_with_size(json.dumps(ans))
|
||||
shm.flush()
|
||||
shm.seek(0)
|
||||
shm.write(b'\x01')
|
||||
finally:
|
||||
shm.close()
|
||||
|
||||
message: str = data['message']
|
||||
window_title = 'A program wants your input'
|
||||
if data['type'] == 'confirm':
|
||||
if q == 'confirm':
|
||||
get_boss().confirm(
|
||||
message,
|
||||
callback,
|
||||
|
|
@ -1833,16 +1850,22 @@ class Window:
|
|||
confirm_on_accept=bool(data.get('confirm_on_accept', True)),
|
||||
title=window_title,
|
||||
)
|
||||
elif data['type'] == 'choose':
|
||||
get_boss().choose(message, callback, *data['choices'], window=self, default=data.get('default', ''), title=window_title)
|
||||
elif data['type'] == 'get_line':
|
||||
elif q == 'choose':
|
||||
get_boss().choose(message, callback, *choices, window=self, default=data.get('default', ''), title=window_title)
|
||||
elif q == 'get_line':
|
||||
which = 'password' if data.get('is_password') else 'input'
|
||||
message = f'\x1b[33mA program running in this window is asking for your {which}\x1b[m\n\n{message}'
|
||||
get_boss().get_line(
|
||||
message, callback, window=self, is_password=bool(data.get('is_password')), prompt=data.get('prompt', '> '), window_title=window_title
|
||||
message,
|
||||
callback,
|
||||
window=self,
|
||||
is_password=bool(data.get('is_password')),
|
||||
prompt=sanitize_control_codes(data.get('prompt', '> ')),
|
||||
window_title=window_title,
|
||||
)
|
||||
else:
|
||||
log_error(f'Ignoring ask request with unknown type: {data["type"]}')
|
||||
shm.close()
|
||||
log_error(f'Ignoring ask request with unknown type: {q}')
|
||||
|
||||
def handle_remote_print(self, msg: memoryview) -> None:
|
||||
text = process_remote_print(msg)
|
||||
|
|
|
|||
|
|
@ -27,3 +27,14 @@ class SHMTest(BaseTest):
|
|||
with SharedMemory(name=name, unlink_on_exit=True) as shm:
|
||||
q = shm.read_data_with_size()
|
||||
self.assertEqual(data, q)
|
||||
|
||||
def test_shm_ownership_verification(self):
|
||||
with SharedMemory(size=64, unlink_on_exit=True) as shm:
|
||||
shm.verify_owner_and_mode()
|
||||
for mode in (0o644, 0o400, 0o660):
|
||||
os.fchmod(shm.fileno(), mode)
|
||||
with SharedMemory(name=shm.name, readonly=True) as shm2:
|
||||
self.assertRaises(ValueError, shm2.verify_owner_and_mode)
|
||||
os.fchmod(shm.fileno(), 0o600)
|
||||
with SharedMemory(name=shm.name, readonly=True) as shm2:
|
||||
shm2.verify_owner_and_mode()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue