From 44073e4c98e9238fdaf4ae7e69cbcfeaa9ec1501 Mon Sep 17 00:00:00 2001 From: David Rubin Date: Fri, 3 Nov 2023 10:58:56 -0700 Subject: [PATCH 1/8] add basic functionality --- src/Surface.zig | 22 ++- src/apprt/gtk/App.zig | 4 + src/apprt/gtk/Surface.zig | 20 ++- src/apprt/gtk/UnsafePasteWindow.zig | 216 ++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 src/apprt/gtk/UnsafePasteWindow.zig diff --git a/src/Surface.zig b/src/Surface.zig index 476359dbe..9865efcfb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2177,7 +2177,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool // If you split it across two then the shell can interpret it // as two literals. var buf: [128]u8 = undefined; - const full_data = try std.fmt.bufPrint(&buf, "\x1b{s}{s}", .{if(action==.csi)"["else"", data}); + const full_data = try std.fmt.bufPrint(&buf, "\x1b{s}{s}", .{ if (action == .csi) "[" else "", data }); _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, full_data, @@ -2457,9 +2457,10 @@ pub fn completeClipboardRequest( self: *Surface, req: apprt.ClipboardRequest, data: []const u8, + force: bool, // Dialog has been shown, and ignoring unsafe pastes. ) !void { switch (req) { - .paste => try self.completeClipboardPaste(data), + .paste => try self.completeClipboardPaste(data, force), .osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind), } } @@ -2485,9 +2486,24 @@ fn startClipboardRequest( try self.rt_surface.clipboardRequest(loc, req); } -fn completeClipboardPaste(self: *Surface, data: []const u8) !void { +fn sanatizeClipboardPaste(data: []const u8) !void { + // Split into lines. + var lines = std.mem.splitSequence(u8, data, "\n"); + + // If there's only one line no need to proceed. + if (std.mem.eql(u8, lines.next().?, data)) return; + + // Warning popup. + return error.UnsafePaste; +} + +fn completeClipboardPaste(self: *Surface, data: []const u8, force: bool) !void { if (data.len == 0) return; + if (!force) { + try sanatizeClipboardPaste(data); + } + const bracketed = bracketed: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 5e5c143b4..68a2d1d0d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -24,6 +24,7 @@ const build_options = @import("build_options"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); +const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const c = @import("c.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -47,6 +48,9 @@ menu: ?*c.GMenu = null, /// The configuration errors window, if it is currently open. config_errors_window: ?*ConfigErrorsWindow = null, +/// The unsafe paste window, if it is currently open. +unsafe_paste_window: ?*UnsafePasteWindow = null, + /// This is set to false when the main loop should exit. running: bool = true, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1ec79184c..1b7fff41a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -17,6 +17,8 @@ const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); +const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); + const log = std.log.scoped(.gtk); /// This is detected by the OpenGL renderer to move to a single-threaded @@ -546,9 +548,22 @@ fn gtkClipboardRead( return; } defer c.g_free(cstr); - const str = std.mem.sliceTo(cstr, 0); - self.core_surface.completeClipboardRequest(req.state, str) catch |err| { + + self.core_surface.completeClipboardRequest(req.state, str, false) catch |err| { + if (err == error.UnsafePaste) { + // Create a dialog and ask the user if they want to paste anyway. + UnsafePasteWindow.create( + self.app, + str, + self.core_surface, + req.state, + ) catch |window_err| { + log.err("failed to create unsafe paste window err={}", .{window_err}); + }; + return; + } + log.err("failed to complete clipboard request err={}", .{err}); }; } @@ -559,7 +574,6 @@ fn getClipboard(widget: *c.GtkWidget, clipboard: apprt.Clipboard) ?*c.GdkClipboa .selection => c.gtk_widget_get_primary_clipboard(widget), }; } - pub fn getCursorPos(self: *const Surface) !apprt.CursorPos { return self.cursor_pos; } diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/UnsafePasteWindow.zig new file mode 100644 index 000000000..081329a09 --- /dev/null +++ b/src/apprt/gtk/UnsafePasteWindow.zig @@ -0,0 +1,216 @@ +/// Configuration errors window. +const UnsafePaste = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const CoreSurface = @import("../../Surface.zig"); +const ClipboardRequest = @import("../structs.zig").ClipboardRequest; +const App = @import("App.zig"); +const View = @import("View.zig"); +const c = @import("c.zig"); + +const log = std.log.scoped(.gtk); + +app: *App, +window: *c.GtkWindow, +view: PrimaryView, + +data: []u8, +core_surface: CoreSurface, +pending_req: ClipboardRequest, + +pub fn create( + app: *App, + data: []const u8, + core_surface: CoreSurface, + request: ClipboardRequest, +) !void { + if (app.unsafe_paste_window != null) return error.WindowAlreadyExists; + + const alloc = app.core_app.alloc; + const self = try alloc.create(UnsafePaste); + errdefer alloc.destroy(self); + try self.init( + app, + data, + core_surface, + request, + ); + + app.unsafe_paste_window = self; +} + +/// Not public because this should be called by the GTK lifecycle. +fn destroy(self: *UnsafePaste) void { + const alloc = self.app.core_app.alloc; + self.app.config_errors_window = null; + alloc.destroy(self); +} + +fn init( + self: *UnsafePaste, + app: *App, + data: []const u8, + core_surface: CoreSurface, + request: ClipboardRequest, +) !void { + // Create the window + const window = c.gtk_window_new(); + const gtk_window: *c.GtkWindow = @ptrCast(window); + errdefer c.gtk_window_destroy(gtk_window); + c.gtk_window_set_title(gtk_window, "Unsafe Paste!"); + c.gtk_window_set_default_size(gtk_window, 600, 400); + c.gtk_window_set_resizable(gtk_window, 0); + _ = c.g_signal_connect_data( + window, + "destroy", + c.G_CALLBACK(>kDestroy), + self, + null, + c.G_CONNECT_DEFAULT, + ); + + // Set some state + self.* = .{ + .app = app, + .window = gtk_window, + .view = undefined, + .data = try app.core_app.alloc.dupe(u8, data), + .core_surface = core_surface, + .pending_req = request, + }; + + // Show the window + const view = try PrimaryView.init(self, data); + self.view = view; + c.gtk_window_set_child(@ptrCast(window), view.root); + c.gtk_widget_show(window); +} + +fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self = userdataSelf(ud.?); + self.destroy(); +} + +fn userdataSelf(ud: *anyopaque) *UnsafePaste { + return @ptrCast(@alignCast(ud)); +} + +const PrimaryView = struct { + root: *c.GtkWidget, + text: *c.GtkTextView, + + pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { + // All our widgets + const label = c.gtk_label_new( + \\ Pasting this text into the terminal may be dangerous as + \\ unintended commands may be be executed. + ); + const buf = unsafeBuffer(data); + defer c.g_object_unref(buf); + const buttons = try ButtonsView.init(root); + const text_scroll = c.gtk_scrolled_window_new(); + errdefer c.g_object_unref(text_scroll); + const text = c.gtk_text_view_new_with_buffer(buf); + errdefer c.g_object_unref(text); + c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text); + + // Create our view + const view = try View.init(&.{ + .{ .name = "label", .widget = label }, + .{ .name = "text", .widget = text_scroll }, + .{ .name = "buttons", .widget = buttons.root }, + }, &vfl); + errdefer view.deinit(); + + // We can do additional settings once the layout is setup + c.gtk_label_set_wrap(@ptrCast(label), 1); + c.gtk_text_view_set_editable(@ptrCast(text), 0); + c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0); + c.gtk_text_view_set_top_margin(@ptrCast(text), 8); + c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8); + c.gtk_text_view_set_left_margin(@ptrCast(text), 8); + c.gtk_text_view_set_right_margin(@ptrCast(text), 8); + + return .{ .root = view.root, .text = @ptrCast(text) }; + } + + /// Returns the GtkTextBuffer for the data that was unsafe. + fn unsafeBuffer(data: []const u8) *c.GtkTextBuffer { + const buf = c.gtk_text_buffer_new(null); + errdefer c.g_object_unref(buf); + + c.gtk_text_buffer_insert_at_cursor(buf, data.ptr, @intCast(data.len)); + + return buf; + } + + const vfl = [_][*:0]const u8{ + "H:|-8-[label]-8-|", + "H:|[text]|", + "H:|[buttons]|", + "V:|[label(<=80)][text(>=100)]-[buttons]-|", + }; +}; + +const ButtonsView = struct { + root: *c.GtkWidget, + + pub fn init(root: *UnsafePaste) !ButtonsView { + const cancel_button = c.gtk_button_new_with_label("Cancel"); + errdefer c.g_object_unref(cancel_button); + + const paste_button = c.gtk_button_new_with_label("Paste"); + errdefer c.g_object_unref(paste_button); + + // Create our view + const view = try View.init(&.{ + .{ .name = "cancel", .widget = cancel_button }, + .{ .name = "paste", .widget = paste_button }, + }, &vfl); + + // Signals + _ = c.g_signal_connect_data( + cancel_button, + "clicked", + c.G_CALLBACK(>kCancelClick), + root, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + paste_button, + "clicked", + c.G_CALLBACK(>kPasteClick), + root, + null, + c.G_CONNECT_DEFAULT, + ); + + return .{ .root = view.root }; + } + + fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self: *UnsafePaste = @ptrCast(@alignCast(ud)); + c.gtk_window_destroy(@ptrCast(self.window)); + + self.app.unsafe_paste_window = null; + } + + fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + const self: *UnsafePaste = @ptrCast(@alignCast(ud)); + + // Requeue the paste, this time forcing it. + self.core_surface.completeClipboardRequest(self.pending_req, self.data, true) catch |err| { + std.log.err("Failed to requeue clipboard request: {}", .{err}); + }; + + c.gtk_window_destroy(@ptrCast(self.window)); + self.app.unsafe_paste_window = null; + } + + const vfl = [_][*:0]const u8{ + "H:[cancel]-8-[paste]-8-|", + }; +}; From 6e575d90359fd71ddf4b80207383b4860a57caae Mon Sep 17 00:00:00 2001 From: David Rubin Date: Fri, 3 Nov 2023 13:20:26 -0700 Subject: [PATCH 2/8] configs added --- src/Surface.zig | 4 +++- src/apprt/gtk/UnsafePasteWindow.zig | 17 +++++++++++------ src/config/Config.zig | 4 ++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 9865efcfb..303c7ee4d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -142,6 +142,7 @@ const DerivedConfig = struct { clipboard_read: bool, clipboard_write: bool, clipboard_trim_trailing_spaces: bool, + clipboard_paste_protection: bool, copy_on_select: configpkg.CopyOnSelect, confirm_close_surface: bool, mouse_interval: u64, @@ -165,6 +166,7 @@ const DerivedConfig = struct { .clipboard_read = config.@"clipboard-read", .clipboard_write = config.@"clipboard-write", .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", + .clipboard_paste_protection = config.@"clipboard-paste-protection", .copy_on_select = config.@"copy-on-select", .confirm_close_surface = config.@"confirm-close-surface", .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms @@ -2500,7 +2502,7 @@ fn sanatizeClipboardPaste(data: []const u8) !void { fn completeClipboardPaste(self: *Surface, data: []const u8, force: bool) !void { if (data.len == 0) return; - if (!force) { + if (!force and self.config.clipboard_paste_protection) { try sanatizeClipboardPaste(data); } diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/UnsafePasteWindow.zig index 081329a09..0d87cc0fb 100644 --- a/src/apprt/gtk/UnsafePasteWindow.zig +++ b/src/apprt/gtk/UnsafePasteWindow.zig @@ -1,4 +1,4 @@ -/// Configuration errors window. +/// Unsafe Paste Window const UnsafePaste = @This(); const std = @import("std"); @@ -44,7 +44,7 @@ pub fn create( /// Not public because this should be called by the GTK lifecycle. fn destroy(self: *UnsafePaste) void { const alloc = self.app.core_app.alloc; - self.app.config_errors_window = null; + self.app.unsafe_paste_window = null; alloc.destroy(self); } @@ -55,12 +55,13 @@ fn init( core_surface: CoreSurface, request: ClipboardRequest, ) !void { + // Create the window const window = c.gtk_window_new(); const gtk_window: *c.GtkWindow = @ptrCast(window); errdefer c.gtk_window_destroy(gtk_window); c.gtk_window_set_title(gtk_window, "Unsafe Paste!"); - c.gtk_window_set_default_size(gtk_window, 600, 400); + c.gtk_window_set_default_size(gtk_window, 400, 275); c.gtk_window_set_resizable(gtk_window, 0); _ = c.g_signal_connect_data( window, @@ -86,6 +87,10 @@ fn init( self.view = view; c.gtk_window_set_child(@ptrCast(window), view.root); c.gtk_widget_show(window); + + // Block the main window from input. + // This will auto-revert when the window is closed. + c.gtk_window_set_modal(gtk_window, 1); } fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { @@ -164,6 +169,9 @@ const ButtonsView = struct { const paste_button = c.gtk_button_new_with_label("Paste"); errdefer c.g_object_unref(paste_button); + // TODO: Focus on the paste button + // c.gtk_widget_grab_focus(paste_button); + // Create our view const view = try View.init(&.{ .{ .name = "cancel", .widget = cancel_button }, @@ -194,8 +202,6 @@ const ButtonsView = struct { fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { const self: *UnsafePaste = @ptrCast(@alignCast(ud)); c.gtk_window_destroy(@ptrCast(self.window)); - - self.app.unsafe_paste_window = null; } fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { @@ -207,7 +213,6 @@ const ButtonsView = struct { }; c.gtk_window_destroy(@ptrCast(self.window)); - self.app.unsafe_paste_window = null; } const vfl = [_][*:0]const u8{ diff --git a/src/config/Config.zig b/src/config/Config.zig index ba29ed6f2..3319f9d09 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -423,6 +423,10 @@ keybind: Keybinds = .{}, /// This does not affect data sent to the clipboard via "clipboard-write". @"clipboard-trim-trailing-spaces": bool = true, +/// Creates a pop-up window when active, warning the user that they are pasting +/// contents that contains more than one line. This could be a "copy paste attack" +@"clipboard-paste-protection": bool = true, + /// The total amount of bytes that can be used for image data (i.e. /// the Kitty image protocol) per terminal scren. The maximum value /// is 4,294,967,295 (4GB). The default is 320MB. If this is set to zero, From a578ec342f570adf601e2ba621d6d5a8603576b8 Mon Sep 17 00:00:00 2001 From: David Rubin Date: Fri, 3 Nov 2023 14:20:24 -0700 Subject: [PATCH 3/8] forgot to update other backends --- src/Surface.zig | 3 ++- src/apprt/embedded.zig | 2 +- src/apprt/glfw.zig | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 303c7ee4d..1307127ad 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1165,7 +1165,7 @@ pub fn keyCallback( /// if bracketed mode is on this will do a bracketed paste. Otherwise, /// this will filter newlines to '\r'. pub fn textCallback(self: *Surface, text: []const u8) !void { - try self.completeClipboardPaste(text); + try self.completeClipboardPaste(text, false); } pub fn focusCallback(self: *Surface, focused: bool) !void { @@ -2463,6 +2463,7 @@ pub fn completeClipboardRequest( ) !void { switch (req) { .paste => try self.completeClipboardPaste(data, force), + // TODO: Support sanaization for OSC 52 .osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind), } } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c4ae49e88..c248c054d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1360,7 +1360,7 @@ pub const CAPI = struct { if (str_len == 0) return; const str = str_ptr[0..str_len]; - ptr.core_surface.completeClipboardRequest(state.*, str) catch |err| { + ptr.core_surface.completeClipboardRequest(state.*, str, false) catch |err| { log.err("error completing clipboard request err={}", .{err}); return; }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index be4103f77..044da8e98 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -589,7 +589,7 @@ pub const Surface = struct { }; // Complete our request - try self.core_surface.completeClipboardRequest(state, str); + try self.core_surface.completeClipboardRequest(state, str, false); } /// Set the clipboard. From 65c9ba0a86c86bafbd80ff705db9d2f78848cbb4 Mon Sep 17 00:00:00 2001 From: David Rubin Date: Sat, 4 Nov 2023 00:50:26 -0700 Subject: [PATCH 4/8] add todos + make sure non-implimented platforms still work. --- src/Surface.zig | 2 +- src/apprt/embedded.zig | 3 ++- src/apprt/glfw.zig | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 1307127ad..3117a4dbe 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1165,7 +1165,7 @@ pub fn keyCallback( /// if bracketed mode is on this will do a bracketed paste. Otherwise, /// this will filter newlines to '\r'. pub fn textCallback(self: *Surface, text: []const u8) !void { - try self.completeClipboardPaste(text, false); + try self.completeClipboardPaste(text, true); } pub fn focusCallback(self: *Surface, focused: bool) !void { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c248c054d..b7ae1c6ee 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1360,7 +1360,8 @@ pub const CAPI = struct { if (str_len == 0) return; const str = str_ptr[0..str_len]; - ptr.core_surface.completeClipboardRequest(state.*, str, false) catch |err| { + // TODO: Support sanaization for MacOS (force: false) + ptr.core_surface.completeClipboardRequest(state.*, str, true) catch |err| { log.err("error completing clipboard request err={}", .{err}); return; }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 044da8e98..83be181ef 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -589,7 +589,8 @@ pub const Surface = struct { }; // Complete our request - try self.core_surface.completeClipboardRequest(state, str, false); + // TODO: Support sanaization for GLFW (force: false) + try self.core_surface.completeClipboardRequest(state, str, true); } /// Set the clipboard. From a38220eaded2a5f8813c650489b897f7771cbaca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 11:19:25 -0700 Subject: [PATCH 5/8] terminal: move sanitization check to this package, unit test --- src/Surface.zig | 38 +++++++++++++++++++++----------------- src/apprt/glfw.zig | 4 ++-- src/config/Config.zig | 7 +++++-- src/terminal/main.zig | 2 ++ 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3117a4dbe..66f5dee70 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2455,15 +2455,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool /// Call this to complete a clipboard request sent to apprt. This should /// only be called once for each request. The data is immediately copied so /// it is safe to free the data after this call. +/// +/// If "allow_unsafe" is false, then the data is checked for "safety" prior. +/// If unsafe data is detected, this will return error.UnsafePaste. Unsafe +/// data is defined as data that contains newlines, though this definition +/// may change later to detect other scenarios. pub fn completeClipboardRequest( self: *Surface, req: apprt.ClipboardRequest, data: []const u8, - force: bool, // Dialog has been shown, and ignoring unsafe pastes. + allow_unsafe: bool, ) !void { switch (req) { - .paste => try self.completeClipboardPaste(data, force), - // TODO: Support sanaization for OSC 52 + .paste => try self.completeClipboardPaste(data, allow_unsafe), .osc_52 => |kind| try self.completeClipboardReadOSC52(data, kind), } } @@ -2489,22 +2493,22 @@ fn startClipboardRequest( try self.rt_surface.clipboardRequest(loc, req); } -fn sanatizeClipboardPaste(data: []const u8) !void { - // Split into lines. - var lines = std.mem.splitSequence(u8, data, "\n"); - - // If there's only one line no need to proceed. - if (std.mem.eql(u8, lines.next().?, data)) return; - - // Warning popup. - return error.UnsafePaste; -} - -fn completeClipboardPaste(self: *Surface, data: []const u8, force: bool) !void { +fn completeClipboardPaste( + self: *Surface, + data: []const u8, + allow_unsafe: bool, +) !void { if (data.len == 0) return; - if (!force and self.config.clipboard_paste_protection) { - try sanatizeClipboardPaste(data); + // If we have paste protection enabled, we detect unsafe pastes and return + // an error. The error approach allows apprt to attempt to complete the paste + // before falling back to requesting confirmation. + if (self.config.clipboard_paste_protection and + !allow_unsafe and + !terminal.isSafePaste(data)) + { + log.info("potentially unsafe paste detected, rejecting until confirmation", .{}); + return error.UnsafePaste; } const bracketed = bracketed: { diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 83be181ef..41bfae892 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -588,8 +588,8 @@ pub const Surface = struct { }, }; - // Complete our request - // TODO: Support sanaization for GLFW (force: false) + // Complete our request. We always allow unsafe because we don't + // want to deal with user confirmation in this runtime. try self.core_surface.completeClipboardRequest(state, str, true); } diff --git a/src/config/Config.zig b/src/config/Config.zig index 3319f9d09..fbb92e5ba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -423,8 +423,11 @@ keybind: Keybinds = .{}, /// This does not affect data sent to the clipboard via "clipboard-write". @"clipboard-trim-trailing-spaces": bool = true, -/// Creates a pop-up window when active, warning the user that they are pasting -/// contents that contains more than one line. This could be a "copy paste attack" +/// Require confirmation before pasting text that appears unsafe. This helps +/// prevent a "copy/paste attack" where a user may accidentally execute unsafe +/// commands by pasting text with newlines. +/// +/// This currently only works on Linux (GTK). @"clipboard-paste-protection": bool = true, /// The total amount of bytes that can be used for image data (i.e. diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 8c0a7fc74..a752d64eb 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -1,5 +1,7 @@ const builtin = @import("builtin"); +pub usingnamespace @import("sanitize.zig"); + const charsets = @import("charsets.zig"); const stream = @import("stream.zig"); const ansi = @import("ansi.zig"); From ab9a9b6eb1427f39efa458d54748bc09bac60910 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 11:24:48 -0700 Subject: [PATCH 6/8] apprt/gtk: copy change for unsafe paste window --- src/apprt/gtk/Surface.zig | 15 +++++++++------ src/apprt/gtk/UnsafePasteWindow.zig | 24 +++++++++++------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1b7fff41a..99acf2f66 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -13,12 +13,11 @@ const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); const Window = @import("Window.zig"); +const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const inspector = @import("inspector.zig"); const gtk_key = @import("key.zig"); const c = @import("c.zig"); -const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); - const log = std.log.scoped(.gtk); /// This is detected by the OpenGL renderer to move to a single-threaded @@ -550,8 +549,12 @@ fn gtkClipboardRead( defer c.g_free(cstr); const str = std.mem.sliceTo(cstr, 0); - self.core_surface.completeClipboardRequest(req.state, str, false) catch |err| { - if (err == error.UnsafePaste) { + self.core_surface.completeClipboardRequest( + req.state, + str, + false, + ) catch |err| switch (err) { + error.UnsafePaste => { // Create a dialog and ask the user if they want to paste anyway. UnsafePasteWindow.create( self.app, @@ -562,9 +565,9 @@ fn gtkClipboardRead( log.err("failed to create unsafe paste window err={}", .{window_err}); }; return; - } + }, - log.err("failed to complete clipboard request err={}", .{err}); + else => log.err("failed to complete clipboard request err={}", .{err}), }; } diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/UnsafePasteWindow.zig index 0d87cc0fb..a41795ee3 100644 --- a/src/apprt/gtk/UnsafePasteWindow.zig +++ b/src/apprt/gtk/UnsafePasteWindow.zig @@ -55,13 +55,12 @@ fn init( core_surface: CoreSurface, request: ClipboardRequest, ) !void { - // Create the window const window = c.gtk_window_new(); const gtk_window: *c.GtkWindow = @ptrCast(window); errdefer c.gtk_window_destroy(gtk_window); - c.gtk_window_set_title(gtk_window, "Unsafe Paste!"); - c.gtk_window_set_default_size(gtk_window, 400, 275); + c.gtk_window_set_title(gtk_window, "Warning: Potentially Unsafe Paste"); + c.gtk_window_set_default_size(gtk_window, 600, 275); c.gtk_window_set_resizable(gtk_window, 0); _ = c.g_signal_connect_data( window, @@ -94,14 +93,10 @@ fn init( } fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { - const self = userdataSelf(ud.?); + const self: *UnsafePaste = @ptrCast(@alignCast(ud orelse return)); self.destroy(); } -fn userdataSelf(ud: *anyopaque) *UnsafePaste { - return @ptrCast(@alignCast(ud)); -} - const PrimaryView = struct { root: *c.GtkWidget, text: *c.GtkTextView, @@ -109,8 +104,8 @@ const PrimaryView = struct { pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { // All our widgets const label = c.gtk_label_new( - \\ Pasting this text into the terminal may be dangerous as - \\ unintended commands may be be executed. + \\ Pasting this text into the terminal may be dangerous as + \\ it looks like some commands may be executed. ); const buf = unsafeBuffer(data); defer c.g_object_unref(buf); @@ -205,10 +200,13 @@ const ButtonsView = struct { } fn gtkPasteClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { + // Requeue the paste with force. const self: *UnsafePaste = @ptrCast(@alignCast(ud)); - - // Requeue the paste, this time forcing it. - self.core_surface.completeClipboardRequest(self.pending_req, self.data, true) catch |err| { + self.core_surface.completeClipboardRequest( + self.pending_req, + self.data, + true, + ) catch |err| { std.log.err("Failed to requeue clipboard request: {}", .{err}); }; From 254365afab7eb2c1cd504e468eb9d22d37770f1f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 11:25:02 -0700 Subject: [PATCH 7/8] terminal: add sanitize.zig --- src/terminal/sanitize.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/terminal/sanitize.zig diff --git a/src/terminal/sanitize.zig b/src/terminal/sanitize.zig new file mode 100644 index 000000000..f492291aa --- /dev/null +++ b/src/terminal/sanitize.zig @@ -0,0 +1,13 @@ +const std = @import("std"); + +/// Returns true if the data looks safe to paste. +pub fn isSafePaste(data: []const u8) bool { + return std.mem.indexOf(u8, data, "\n") == null; +} + +test isSafePaste { + const testing = std.testing; + try testing.expect(isSafePaste("hello")); + try testing.expect(!isSafePaste("hello\n")); + try testing.expect(!isSafePaste("hello\nworld")); +} From 43e1c9f14749bc035c6c07e231ccb0599992bda0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Nov 2023 11:27:39 -0700 Subject: [PATCH 8/8] apprt/gtk: tweak unsafe paste window --- src/apprt/gtk/UnsafePasteWindow.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/UnsafePasteWindow.zig b/src/apprt/gtk/UnsafePasteWindow.zig index a41795ee3..e3582e892 100644 --- a/src/apprt/gtk/UnsafePasteWindow.zig +++ b/src/apprt/gtk/UnsafePasteWindow.zig @@ -60,7 +60,7 @@ fn init( const gtk_window: *c.GtkWindow = @ptrCast(window); errdefer c.gtk_window_destroy(gtk_window); c.gtk_window_set_title(gtk_window, "Warning: Potentially Unsafe Paste"); - c.gtk_window_set_default_size(gtk_window, 600, 275); + c.gtk_window_set_default_size(gtk_window, 550, 275); c.gtk_window_set_resizable(gtk_window, 0); _ = c.g_signal_connect_data( window, @@ -104,8 +104,8 @@ const PrimaryView = struct { pub fn init(root: *UnsafePaste, data: []const u8) !PrimaryView { // All our widgets const label = c.gtk_label_new( - \\ Pasting this text into the terminal may be dangerous as - \\ it looks like some commands may be executed. + "Pasting this text into the terminal may be dangerous as " ++ + "it looks like some commands may be executed.", ); const buf = unsafeBuffer(data); defer c.g_object_unref(buf);