mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-17 05:54:59 +02:00
Prevent reading uri-list items for a drag and drop in same window
This commit is contained in:
@@ -158,6 +158,12 @@ must respond with ``t=R ; EINVAL`` if the file is not a regular file after
|
|||||||
resolving symlinks and ``t=R ; ENOENT`` if the file does not exist. If an
|
resolving symlinks and ``t=R ; ENOENT`` if the file does not exist. If an
|
||||||
I/O error occurs the terminal must send ``t=R ; EIO``.
|
I/O error occurs the terminal must send ``t=R ; EIO``.
|
||||||
|
|
||||||
|
For security reasons, terminals must reply with ``t=R ; EPERM`` if the drag
|
||||||
|
originated in the same window as the drop, this prevents malicious programs
|
||||||
|
from reading files on the computer by starting their own drag. This is a
|
||||||
|
defense in depth feature since drags can only be started by the terminal, but
|
||||||
|
it helps in case of accidental drag starts and drops into the same window.
|
||||||
|
|
||||||
|
|
||||||
Reading remote directories
|
Reading remote directories
|
||||||
+++++++++++++++++++++++++++
|
+++++++++++++++++++++++++++
|
||||||
@@ -309,6 +315,9 @@ If the client wants to cancel the full drag at any time, it should send:
|
|||||||
|
|
||||||
OSC _dnd_code ; t=E:y=-1 ST
|
OSC _dnd_code ; t=E:y=-1 ST
|
||||||
|
|
||||||
|
If ``t=e`` or ``t=E`` escape codes are sent to the terminal before the drag is
|
||||||
|
started and the terminal replies with ``t=R ; OK``, the terminal must respond
|
||||||
|
with ``t=R ; EINVAL`` and abort the drag.
|
||||||
|
|
||||||
Multiplexers
|
Multiplexers
|
||||||
-----------------
|
-----------------
|
||||||
|
|||||||
12
kitty/dnd.c
12
kitty/dnd.c
@@ -752,6 +752,9 @@ drop_request_uri_data(Window *w, const char *payload, size_t payload_sz) {
|
|||||||
if (!w->drop.uri_list || !w->drop.uri_list_sz) {
|
if (!w->drop.uri_list || !w->drop.uri_list_sz) {
|
||||||
drop_send_error(w, EINVAL); return;
|
drop_send_error(w, EINVAL); return;
|
||||||
}
|
}
|
||||||
|
if (global_state.drag_source.from_window == w->id && w->drag_source.state != DRAG_SOURCE_NONE) {
|
||||||
|
drop_send_error(w, EPERM); return;
|
||||||
|
}
|
||||||
|
|
||||||
/* Payload format: "text/uri-list:idx" */
|
/* Payload format: "text/uri-list:idx" */
|
||||||
const char *colon = memchr(payload, ':', payload_sz);
|
const char *colon = memchr(payload, ':', payload_sz);
|
||||||
@@ -1070,6 +1073,7 @@ drag_start(Window *w) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
drag_notify(Window *w, DragNotifyType type) {
|
drag_notify(Window *w, DragNotifyType type) {
|
||||||
|
if (ds.state < DRAG_SOURCE_STARTED) return;
|
||||||
char buf[128];
|
char buf[128];
|
||||||
size_t sz = snprintf(buf, sizeof(buf), "t=e:x=%d", type + 1);
|
size_t sz = snprintf(buf, sizeof(buf), "t=e:x=%d", type + 1);
|
||||||
switch(type) {
|
switch(type) {
|
||||||
@@ -1091,6 +1095,7 @@ drag_notify(Window *w, DragNotifyType type) {
|
|||||||
sz += snprintf(buf + sz, sizeof(buf) - sz, "y=%d", global_state.drag_source.was_canceled ? 1 : 0); break;
|
sz += snprintf(buf + sz, sizeof(buf) - sz, "y=%d", global_state.drag_source.was_canceled ? 1 : 0); break;
|
||||||
}
|
}
|
||||||
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, sz, NULL, 0, false);
|
queue_payload_to_child(w->id, w->drag_source.client_id, &w->drag_source.pending, buf, sz, NULL, 0, false);
|
||||||
|
if (type == DRAG_NOTIFY_FINISHED) drag_free_offer(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
@@ -1103,7 +1108,7 @@ drag_free_data(Window *w, const char *mime_type, const char* data, size_t sz) {
|
|||||||
const char*
|
const char*
|
||||||
drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
|
drag_get_data(Window *w, const char *mime_type, size_t *sz, int *err_code) {
|
||||||
*err_code = ENOENT; *sz = 0;
|
*err_code = ENOENT; *sz = 0;
|
||||||
if (!ds.items) return NULL;
|
if (!ds.items || ds.state < DRAG_SOURCE_DROPPED) return NULL;
|
||||||
for (size_t i = 0; i < ds.num_mimes; i++) {
|
for (size_t i = 0; i < ds.num_mimes; i++) {
|
||||||
if (strcmp(ds.items[i].mime_type, mime_type) == 0) {
|
if (strcmp(ds.items[i].mime_type, mime_type) == 0) {
|
||||||
if (ds.items[i].fd_plus_one < 0) {
|
if (ds.items[i].fd_plus_one < 0) {
|
||||||
@@ -1186,7 +1191,10 @@ open_item_tmpfile(void) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz) {
|
drag_process_item_data(Window *w, size_t idx, int has_more, const uint8_t *payload, size_t payload_sz) {
|
||||||
if ((ds.state != DRAG_SOURCE_STARTED && ds.state != DRAG_SOURCE_DROPPED) || idx >= ds.num_mimes || !ds.items) return;
|
if ((ds.state < DRAG_SOURCE_DROPPED) || idx >= ds.num_mimes || !ds.items) {
|
||||||
|
abrt(EINVAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (has_more < 0) {
|
if (has_more < 0) {
|
||||||
// Error from the client program
|
// Error from the client program
|
||||||
|
|||||||
@@ -1297,22 +1297,22 @@ class TestDnDProtocol(BaseTest):
|
|||||||
parse_bytes(screen, final_img)
|
parse_bytes(screen, final_img)
|
||||||
self._assert_no_output(cap, wid)
|
self._assert_no_output(cap, wid)
|
||||||
|
|
||||||
def test_drag_process_item_data_without_started_state_ignored(self) -> None:
|
def test_drag_process_item_data_without_started_state_invalid(self) -> None:
|
||||||
"""Sending t=e data before the drag is started is silently ignored."""
|
"""Sending t=e data before the drag is started is silently ignored."""
|
||||||
with dnd_test_window() as (osw, wid, screen, cap):
|
with dnd_test_window() as (osw, wid, screen, cap):
|
||||||
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
||||||
# State is BEING_BUILT, not STARTED – drag_process_item_data should return early
|
# State is BEING_BUILT, not STARTED – drag_process_item_data should return early
|
||||||
data_b64 = standard_b64encode(b'premature data').decode()
|
data_b64 = standard_b64encode(b'premature data').decode()
|
||||||
parse_bytes(screen, client_drag_send_data(0, data_b64))
|
parse_bytes(screen, client_drag_send_data(0, data_b64))
|
||||||
self._assert_no_output(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
|
|
||||||
def test_drag_error_from_client_without_started_state_ignored(self) -> None:
|
def test_drag_error_from_client_without_started_state_invalid(self) -> None:
|
||||||
"""Sending t=E with a MIME index before the drag is started is silently ignored."""
|
"""Sending t=E with a MIME index before the drag is started is silently ignored."""
|
||||||
with dnd_test_window() as (osw, wid, screen, cap):
|
with dnd_test_window() as (osw, wid, screen, cap):
|
||||||
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
||||||
# State is BEING_BUILT – sending an error for index 0 should be ignored
|
# State is BEING_BUILT – sending an error for index 0 should be ignored
|
||||||
parse_bytes(screen, client_drag_send_error(0, 'EIO'))
|
parse_bytes(screen, client_drag_send_error(0, 'EIO'))
|
||||||
self._assert_no_output(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
|
|
||||||
def test_drag_offer_with_empty_mimes_after_cancel(self) -> None:
|
def test_drag_offer_with_empty_mimes_after_cancel(self) -> None:
|
||||||
"""After cancelling, a new offer can be started from scratch."""
|
"""After cancelling, a new offer can be started from scratch."""
|
||||||
@@ -1354,10 +1354,13 @@ class TestDnDProtocol(BaseTest):
|
|||||||
|
|
||||||
# Attempting to start should fail since unregister called drag_free_offer
|
# Attempting to start should fail since unregister called drag_free_offer
|
||||||
parse_bytes(screen, client_drag_start())
|
parse_bytes(screen, client_drag_start())
|
||||||
events = self._get_events(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
self.assertEqual(len(events), 1, events)
|
|
||||||
self.ae(events[0]['type'], 'R')
|
def assert_error(self, cap, wid, code='EINVAL'):
|
||||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
events = self._get_events(cap, wid)
|
||||||
|
self.assertEqual(len(events), 1, events)
|
||||||
|
self.ae(events[0]['type'], 'R')
|
||||||
|
self.ae(events[0]['payload'].strip(), code.encode())
|
||||||
|
|
||||||
def test_drag_pre_send_multiple_mimes(self) -> None:
|
def test_drag_pre_send_multiple_mimes(self) -> None:
|
||||||
"""Pre-sent data can be provided for multiple different MIME types."""
|
"""Pre-sent data can be provided for multiple different MIME types."""
|
||||||
@@ -1429,10 +1432,7 @@ class TestDnDProtocol(BaseTest):
|
|||||||
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
||||||
# Send completely invalid base64
|
# Send completely invalid base64
|
||||||
parse_bytes(screen, client_drag_pre_send(0, '!@#$%^&*()'))
|
parse_bytes(screen, client_drag_pre_send(0, '!@#$%^&*()'))
|
||||||
events = self._get_events(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
self.assertEqual(len(events), 1, events)
|
|
||||||
self.ae(events[0]['type'], 'R')
|
|
||||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
|
||||||
|
|
||||||
def test_drag_add_image_invalid_base64_returns_einval(self) -> None:
|
def test_drag_add_image_invalid_base64_returns_einval(self) -> None:
|
||||||
"""Adding an image with invalid base64 data returns EINVAL."""
|
"""Adding an image with invalid base64 data returns EINVAL."""
|
||||||
@@ -1440,10 +1440,7 @@ class TestDnDProtocol(BaseTest):
|
|||||||
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
self._setup_drag_offer(screen, wid, cap, 'text/plain')
|
||||||
# Invalid base64 as image data
|
# Invalid base64 as image data
|
||||||
parse_bytes(screen, client_drag_add_image(1, 32, 1, 1, '!@#$%^&*()'))
|
parse_bytes(screen, client_drag_add_image(1, 32, 1, 1, '!@#$%^&*()'))
|
||||||
events = self._get_events(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
self.assertEqual(len(events), 1, events)
|
|
||||||
self.ae(events[0]['type'], 'R')
|
|
||||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
|
||||||
|
|
||||||
def test_drag_start_with_image_size_mismatch(self) -> None:
|
def test_drag_start_with_image_size_mismatch(self) -> None:
|
||||||
"""Starting a drag when image data size doesn't match dimensions returns EINVAL."""
|
"""Starting a drag when image data size doesn't match dimensions returns EINVAL."""
|
||||||
@@ -1459,10 +1456,7 @@ class TestDnDProtocol(BaseTest):
|
|||||||
# Actually no - for fmt=32, expand_rgb_data is not called, only for fmt=24.
|
# Actually no - for fmt=32, expand_rgb_data is not called, only for fmt=24.
|
||||||
# The check img.sz != width*height*4 happens in drag_start.
|
# The check img.sz != width*height*4 happens in drag_start.
|
||||||
parse_bytes(screen, client_drag_start())
|
parse_bytes(screen, client_drag_start())
|
||||||
events = self._get_events(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
self.assertEqual(len(events), 1, events)
|
|
||||||
self.ae(events[0]['type'], 'R')
|
|
||||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
|
||||||
|
|
||||||
def test_drag_start_with_rgb_image_size_mismatch(self) -> None:
|
def test_drag_start_with_rgb_image_size_mismatch(self) -> None:
|
||||||
"""Starting a drag when RGB image data size doesn't match w*h*3 returns EINVAL."""
|
"""Starting a drag when RGB image data size doesn't match w*h*3 returns EINVAL."""
|
||||||
@@ -1474,8 +1468,4 @@ class TestDnDProtocol(BaseTest):
|
|||||||
parse_bytes(screen, client_drag_add_image(1, 24, 2, 2, data_b64))
|
parse_bytes(screen, client_drag_add_image(1, 24, 2, 2, data_b64))
|
||||||
# drag_start calls expand_rgb_data which checks sz == w*h*3
|
# drag_start calls expand_rgb_data which checks sz == w*h*3
|
||||||
parse_bytes(screen, client_drag_start())
|
parse_bytes(screen, client_drag_start())
|
||||||
events = self._get_events(cap, wid)
|
self.assert_error(cap, wid)
|
||||||
self.assertEqual(len(events), 1, events)
|
|
||||||
self.ae(events[0]['type'], 'R')
|
|
||||||
self.ae(events[0]['payload'].strip(), b'EINVAL')
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user