Prevent reading uri-list items for a drag and drop in same window

This commit is contained in:
Kovid Goyal
2026-04-09 06:38:58 +05:30
parent 6c35b64356
commit 47b5b2ea65
3 changed files with 34 additions and 27 deletions

View File

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

View File

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

View File

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