Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement opt-in OSC52 clipboard querying #6239

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions codec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ pdu! {
SendPaste: 13,
Resize: 14,
SetClipboard: 20,
QueryClipboard: 21,
QueryClipboardResponse: 63,
GetLines: 22,
GetLinesResponse: 23,
GetPaneRenderChanges: 24,
Expand Down Expand Up @@ -516,6 +518,7 @@ impl Pdu {
| Self::SendPaste(_)
| Self::Resize(_)
| Self::SetClipboard(_)
| Self::QueryClipboard(_)
| Self::SetPaneZoomed(_)
| Self::SpawnV2(_) => true,
_ => false,
Expand Down Expand Up @@ -594,6 +597,7 @@ impl Pdu {
| Pdu::SetPalette(SetPalette { pane_id, .. })
| Pdu::NotifyAlert(NotifyAlert { pane_id, .. })
| Pdu::SetClipboard(SetClipboard { pane_id, .. })
| Pdu::QueryClipboard(QueryClipboard { pane_id, .. })
| Pdu::PaneFocused(PaneFocused { pane_id })
| Pdu::PaneRemoved(PaneRemoved { pane_id }) => Some(*pane_id),
_ => None,
Expand Down Expand Up @@ -769,6 +773,18 @@ pub struct SetClipboard {
pub selection: ClipboardSelection,
}

#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct QueryClipboard {
pub pane_id: PaneId,
pub selection: ClipboardSelection,
}

#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct QueryClipboardResponse {
pub pane_id: PaneId,
pub content: Option<String>,
}

#[derive(Deserialize, Serialize, PartialEq, Debug)]
pub struct SetWindowWorkspace {
pub window_id: WindowId,
Expand Down
6 changes: 6 additions & 0 deletions config/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,12 @@ pub struct Config {
#[dynamic(default)]
pub enable_kitty_keyboard: bool,

/// Whether the terminal should respond to OSC 52 requests to read the
/// clipboard content.
/// Disabled by default for security concerns.
#[dynamic(default)]
pub enable_osc52_clipboard_reading: bool,

/// Whether the terminal should respond to requests to read the
/// title string.
/// Disabled by default for security concerns with shells that might
Expand Down
4 changes: 4 additions & 0 deletions config/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ impl wezterm_term::TerminalConfiguration for TermConfig {
self.configuration().enable_kitty_keyboard
}

fn enable_osc52_clipboard_reading(&self) -> bool {
self.configuration().enable_osc52_clipboard_reading
}

fn canonicalize_pasted_newlines(&self) -> wezterm_term::config::NewlineCanon {
match self.configuration().canonicalize_pasted_newlines {
None => wezterm_term::config::NewlineCanon::default(),
Expand Down
30 changes: 30 additions & 0 deletions docs/config/lua/config/enable_osc52_clipboard_reading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
tags:
- osc
- clipboard
---

# `enable_osc52_clipboard_reading = false`

{{since('nightly')}}

When set to `true`, the terminal will allow access to the system clipboard by
terminal applications via `OSC 52` [escape sequence](../../../shell-integration.md#osc-52-clipboard-paste).

The default for this option is `false`.

Note that it is not recommended to enable this option due to serious security
implications.

### Security Concerns

Clipboards are often used to store sensitive information, and granting any
terminal application (especially from remote machines) access to it poses a
security risk. A malicious server could spam the terminal with OSC 52 paste
sequences to monitor whatever you have on the clipboard, which may occasionally
contain sensitive data.

Setting clipboard data that contains escape sequences or malicious commands and
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, since I don't understand. Is this still valid even using ssh (encrypted tunnel)? How can an attacker inject harmful characters without already knowing how to access to the encrypted tunnel?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They probably can't, but OSC 52 is a way to access the local clipboard from a remote location. If it's not your server — for example a telnet multiplayer game or netcat .. — those servers could easily access your personal data. I doubt they could execute anything, though, as the data is base64-encoded.

reading it back could allow an attacker to inject harmful characters into the
input stream. Although, the risk is somewhat mitigated as the pasted text is
encoded in BASE64.
31 changes: 31 additions & 0 deletions docs/shell-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ wezterm supports integrating with the shell through the following means:
* `OSC 7` Escape sequences to advise the terminal of the working directory
* `OSC 133` Escape sequence to define Input, Output and Prompt zones
* `OSC 1337` Escape sequences to set user vars for tracking additional shell state
* `OSC 52` Escape sequences for writing to and reading from the clipboard

`OSC` is escape sequence jargon for *Operating System Command*.

Expand Down Expand Up @@ -180,3 +181,33 @@ return config

Now, rather than just running `cmd.exe` on its own, this will cause `cmd.exe`
to self-inject the clink line editor.

## OSC 52 Clipboard Copy

The data can be copied to the `System Clipboard` or `Primary Selection` with a
command like this:

```bash
printf '\033]52;c;%s\033\\' $(base64 <<< "hello world")
```

- `c` copies to the system clipboard
- `p` copies to the primary selection buffer

The second parameter provides the selection data, which is a string encoded in base64 (RFC-4648).

## OSC 52 Clipboard Paste

The data can be pasted from the `System Clipboard` or `Primary Selection` with a
command like this:

```bash
printf "\033]7;c;?\033\\"
39555 marked this conversation as resolved.
Show resolved Hide resolved
```

- `c` pastes to the system clipboard
- `p` pastes to the primary selection buffer
39555 marked this conversation as resolved.
Show resolved Hide resolved

Note that this feature poses a potential security risk and is disabled by
default. It requires enabling via [a configuration option](config/lua/config/enable_osc52_clipboard_reading.md).
You should carefully consider whether you're willing to accept the associated risks before enabling it.
22 changes: 21 additions & 1 deletion mux/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use std::time::{Duration, Instant};
use termwiz::escape::csi::{DecPrivateMode, DecPrivateModeCode, Device, Mode};
use termwiz::escape::{Action, CSI};
use thiserror::*;
use wezterm_term::{Clipboard, ClipboardSelection, DownloadHandler, TerminalSize};
use wezterm_term::{Clipboard, ClipboardReader, ClipboardSelection, DownloadHandler, TerminalSize};
#[cfg(windows)]
use winapi::um::winsock2::{SOL_SOCKET, SO_RCVBUF, SO_SNDBUF};

Expand Down Expand Up @@ -71,6 +71,11 @@ pub enum MuxNotification {
selection: ClipboardSelection,
clipboard: Option<String>,
},
QueryClipboard {
pane_id: PaneId,
selection: ClipboardSelection,
writer: Box<dyn ClipboardReader>,
},
SaveToDownloads {
name: Option<String>,
data: Arc<Vec<u8>>,
Expand Down Expand Up @@ -1435,6 +1440,21 @@ impl Clipboard for MuxClipboard {
});
Ok(())
}
fn get_contents(
&self,
selection: ClipboardSelection,
writer: Box<dyn ClipboardReader>,
) -> anyhow::Result<()> {
let mux =
Mux::try_get().ok_or_else(|| anyhow::anyhow!("MuxClipboard::get_contents: no Mux?"))?;

mux.notify(MuxNotification::QueryClipboard {
pane_id: self.pane_id,
selection,
writer,
});
Ok(())
}
}

struct MuxDownloader {}
Expand Down
4 changes: 4 additions & 0 deletions term/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ pub trait TerminalConfiguration: Downcast + std::fmt::Debug + Send + Sync {
false
}

fn enable_osc52_clipboard_reading(&self) -> bool {
false
}

/// The default unicode version to assume.
/// This affects how the width of certain sequences is interpreted.
/// At the time of writing, we default to 9 even though the current
Expand Down
39 changes: 39 additions & 0 deletions term/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,44 @@ pub enum ClipboardSelection {
PrimarySelection,
}

/// A special trait that defines an interface for reading the clipboard content via [`Clipboard::get_contents`].
/// Once the content is available, it will be written to the provided [`Box<dyn ClipboardReader>`].
pub trait ClipboardReader: Send + Sync + std::fmt::Debug + ClipboardReaderBoxClone {
/// Similar to [`std::io::Write`] but receives a full String instead of bytes
fn write(&mut self, contents: String) -> Result<(), std::io::Error>;
}

/// Allow [`ClipboardReader`] to be [`Clone`] when used within [`Box<dyn ClipboardReader>`]
pub trait ClipboardReaderBoxClone {
fn clone_box(&self) -> Box<dyn ClipboardReader>;
}
// Automatically implement for all `dyn ClipboardReader` types that also implement `Clone`
impl<T> ClipboardReaderBoxClone for T
where
T: 'static + ClipboardReader + Clone,
{
fn clone_box(&self) -> Box<dyn ClipboardReader> {
Box::new(self.clone())
}
}

impl Clone for Box<dyn ClipboardReader> {
fn clone(&self) -> Self {
self.clone_box()
}
}

pub trait Clipboard: Send + Sync {
fn set_contents(
&self,
selection: ClipboardSelection,
data: Option<String>,
) -> anyhow::Result<()>;
fn get_contents(
&self,
selection: ClipboardSelection,
writer: Box<dyn ClipboardReader>,
) -> anyhow::Result<()>;
}

impl Clipboard for Box<dyn Clipboard> {
Expand All @@ -26,6 +58,13 @@ impl Clipboard for Box<dyn Clipboard> {
) -> anyhow::Result<()> {
self.as_ref().set_contents(selection, data)
}
fn get_contents(
&self,
selection: ClipboardSelection,
writer: Box<dyn ClipboardReader>,
) -> anyhow::Result<()> {
self.as_ref().get_contents(selection, writer)
}
}

pub trait DeviceControlHandler: Send + Sync {
Expand Down
1 change: 1 addition & 0 deletions term/src/terminalstate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ fn default_color_map() -> HashMap<u16, RgbColor> {
/// back-pressure when there is a lot of data to read,
/// and we're in control of the write side, which represents
/// input from the interactive user, or pastes.
#[derive(Debug, Clone)]
struct ThreadedWriter {
sender: Sender<WriterMessage>,
}
Expand Down
39 changes: 38 additions & 1 deletion term/src/terminalstate/performer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ use unicode_normalization::{is_nfc_quick, IsNormalized, UnicodeNormalization};
use url::Url;
use wezterm_bidi::ParagraphDirectionHint;

use super::{ClipboardReader, ThreadedWriter};

/// A helper struct for implementing `vtparse::VTActor` while compartmentalizing
/// the terminal state and the embedding/host terminal interface
pub(crate) struct Performer<'a> {
Expand Down Expand Up @@ -765,7 +767,26 @@ impl<'a> Performer<'a> {
let selection = selection_to_selection(selection);
self.set_clipboard_contents(selection, None).ok();
}
OperatingSystemCommand::QuerySelection(_) => {}
OperatingSystemCommand::QuerySelection(selection) => {
if self.config.enable_osc52_clipboard_reading() {
if let Some(clip) = self.clipboard.as_ref() {
let clipboard = selection_to_selection(selection);
if let Err(err) = clip.get_contents(
clipboard,
Box::new(Osc52ResponseSender(
self.writer.get_ref().clone(),
selection,
)),
) {
error!("failed to get clipboard in response to OSC 52: {:#?}", err);
}
} else {
log::warn!("the clipboard is missing");
}
} else {
log::warn!("OSC52 clipboard reading is disabled");
}
}
OperatingSystemCommand::SetSelection(selection, selection_data) => {
let selection = selection_to_selection(selection);
match self.set_clipboard_contents(selection, Some(selection_data)) {
Expand Down Expand Up @@ -1071,3 +1092,19 @@ fn selection_to_selection(sel: Selection) -> ClipboardSelection {
_ => ClipboardSelection::Clipboard,
}
}

/// Implement [`ClipboardReader`] and wrap the content in OSC 52 sequence
/// before sending it to the [`ThreadedWriter`]
#[derive(Debug, Clone)]
struct Osc52ResponseSender(ThreadedWriter, Selection);

impl ClipboardReader for Osc52ResponseSender {
fn write(&mut self, contents: String) -> Result<(), std::io::Error> {
// NOTE: reply should be sent in one single write. Multiple writes
// cause wrong interpretation of the osc sequence,
// for example in neovim or micro editors
let data = format!("{}", OperatingSystemCommand::SetSelection(self.1, contents));
self.0.write_all(data.as_bytes())?;
self.0.flush()
}
}
Loading