agent: code-atomiser-fix — broaden clipboard/paste guards to documented secondary windows
The Trust-3/Trust-6 fix in commit 7aee534 added ensure_main_window to
copy_to_clipboard, paste_text, and paste_text_replacing. That was over-
broad: the transcript-viewer and transcription-preview windows
legitimately call copy_to_clipboard (their "copy raw" / "copy
transcript" buttons), and the transcription-preview window legitimately
calls paste_text_replacing (the preview paste-to-foreground flow).
The original Trust-3/Trust-6 finding was about asymmetric exposure to
arbitrary windows, not about banning the documented secondary windows.
Fix: add ensure_window_in_set helper in commands::security, mirror the
allow-list against src-tauri/capabilities/secondary-windows.json so the
IPC trust boundary and the permission grant stay in lock-step.
copy_to_clipboard -> main + transcript-viewer + transcription-preview
paste_text_replacing -> main + transcription-preview
paste_text -> main only (unchanged; no secondary caller)
Adds two unit tests for ensure_window_in_set_label covering the
listed-label accept path and the unlisted-label reject path. The
existing Trust-3/Trust-6 tests remain in place and continue to assert
the size cap and the constants-equality invariant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use arboard::Clipboard;
|
use arboard::Clipboard;
|
||||||
|
|
||||||
use crate::commands::security::ensure_main_window;
|
use crate::commands::security::ensure_window_in_set;
|
||||||
|
|
||||||
/// Refuse-to-set-clipboard limit. 1 MiB is comfortably above any
|
/// Refuse-to-set-clipboard limit. 1 MiB is comfortably above any
|
||||||
/// dictation output and any sensible "copy raw transcript" — anything
|
/// dictation output and any sensible "copy raw transcript" — anything
|
||||||
@@ -11,13 +11,24 @@ use crate::commands::security::ensure_main_window;
|
|||||||
/// identically.
|
/// identically.
|
||||||
pub(crate) const MAX_CLIPBOARD_BYTES: usize = 1024 * 1024;
|
pub(crate) const MAX_CLIPBOARD_BYTES: usize = 1024 * 1024;
|
||||||
|
|
||||||
|
/// Windows that are allowed to call `copy_to_clipboard`. The transcript
|
||||||
|
/// viewer and transcription preview both have legitimate "copy raw text"
|
||||||
|
/// buttons; mirror the secondary-windows capability grant in
|
||||||
|
/// `src-tauri/capabilities/secondary-windows.json` so the IPC trust
|
||||||
|
/// boundary and the permission set stay in lock-step.
|
||||||
|
const CLIPBOARD_ALLOWED_WINDOWS: &[&str] =
|
||||||
|
&["main", "transcript-viewer", "transcription-preview"];
|
||||||
|
|
||||||
/// Copy text to the system clipboard via arboard. Restricted to the
|
/// Copy text to the system clipboard via arboard. Restricted to the
|
||||||
/// main window and capped at `MAX_CLIPBOARD_BYTES` to keep the IPC
|
/// documented "clipboard-capable" windows (main + transcript-viewer +
|
||||||
/// surface symmetric with the rest of the codebase (Trust-6,
|
/// transcription-preview) and capped at `MAX_CLIPBOARD_BYTES` to keep the
|
||||||
/// 2026-05-12 code-atomiser fix).
|
/// IPC surface symmetric with the rest of the codebase (Trust-6,
|
||||||
|
/// 2026-05-12 code-atomiser fix; broadened on 2026-05-13 to cover the
|
||||||
|
/// in-app preview/viewer copy buttons that were collateral-damaged by the
|
||||||
|
/// original main-only restriction).
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn copy_to_clipboard(window: tauri::WebviewWindow, text: String) -> Result<(), String> {
|
pub fn copy_to_clipboard(window: tauri::WebviewWindow, text: String) -> Result<(), String> {
|
||||||
ensure_main_window(&window)?;
|
ensure_window_in_set(&window, CLIPBOARD_ALLOWED_WINDOWS)?;
|
||||||
if text.len() > MAX_CLIPBOARD_BYTES {
|
if text.len() > MAX_CLIPBOARD_BYTES {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Clipboard payload too large ({} bytes; limit {} bytes).",
|
"Clipboard payload too large ({} bytes; limit {} bytes).",
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ use arboard::Clipboard;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
use crate::commands::security::ensure_main_window;
|
use crate::commands::security::{ensure_main_window, ensure_window_in_set};
|
||||||
|
|
||||||
|
/// Windows allowed to invoke `paste_text_replacing`. The
|
||||||
|
/// `transcription-preview` window's paste-to-foreground flow legitimately
|
||||||
|
/// uses this command; mirror the secondary-windows capability grant in
|
||||||
|
/// `src-tauri/capabilities/secondary-windows.json`.
|
||||||
|
const PASTE_REPLACING_ALLOWED_WINDOWS: &[&str] = &["main", "transcription-preview"];
|
||||||
|
|
||||||
/// Refuse-to-paste limit. Matches `commands::clipboard::MAX_CLIPBOARD_BYTES`
|
/// Refuse-to-paste limit. Matches `commands::clipboard::MAX_CLIPBOARD_BYTES`
|
||||||
/// (1 MiB) so paste and copy surfaces share a single rejection rule.
|
/// (1 MiB) so paste and copy surfaces share a single rejection rule.
|
||||||
@@ -208,7 +214,7 @@ pub async fn paste_text_replacing(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
text: String,
|
text: String,
|
||||||
) -> Result<PasteOutcome, String> {
|
) -> Result<PasteOutcome, String> {
|
||||||
ensure_main_window(&window)?;
|
ensure_window_in_set(&window, PASTE_REPLACING_ALLOWED_WINDOWS)?;
|
||||||
if text.len() > MAX_PASTE_BYTES {
|
if text.len() > MAX_PASTE_BYTES {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Paste payload too large ({} bytes; limit {} bytes).",
|
"Paste payload too large ({} bytes; limit {} bytes).",
|
||||||
|
|||||||
@@ -12,9 +12,35 @@ pub fn ensure_main_window_label(label: &str) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reject the call unless the invoking window's label is one of `allowed`.
|
||||||
|
///
|
||||||
|
/// Use this for commands that are legitimately reachable from a small set of
|
||||||
|
/// documented secondary windows (e.g. the transcript viewer or the
|
||||||
|
/// transcription preview), where `ensure_main_window` would be over-broad
|
||||||
|
/// and break in-app flows. The allowed set should always be a fixed
|
||||||
|
/// `&[&'static str]` and should mirror an entry in
|
||||||
|
/// `src-tauri/capabilities/*.json` — keeping the IPC trust boundary and the
|
||||||
|
/// permission grant in lock-step.
|
||||||
|
pub fn ensure_window_in_set(
|
||||||
|
window: &tauri::WebviewWindow,
|
||||||
|
allowed: &[&str],
|
||||||
|
) -> Result<(), String> {
|
||||||
|
ensure_window_in_set_label(window.label(), allowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_window_in_set_label(label: &str, allowed: &[&str]) -> Result<(), String> {
|
||||||
|
if allowed.iter().any(|a| *a == label) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"This command is only available from {allowed:?} (got {label})."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::ensure_main_window_label;
|
use super::{ensure_main_window_label, ensure_window_in_set_label};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_main_window() {
|
fn accepts_main_window() {
|
||||||
@@ -27,4 +53,19 @@ mod tests {
|
|||||||
assert!(ensure_main_window_label(label).is_err());
|
assert!(ensure_main_window_label(label).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_window_in_set_accepts_listed_labels() {
|
||||||
|
let allowed = &["main", "transcript-viewer", "transcription-preview"];
|
||||||
|
for label in allowed {
|
||||||
|
assert!(ensure_window_in_set_label(label, allowed).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_window_in_set_rejects_unlisted_label() {
|
||||||
|
let allowed = &["main", "transcript-viewer"];
|
||||||
|
assert!(ensure_window_in_set_label("tasks-float", allowed).is_err());
|
||||||
|
assert!(ensure_window_in_set_label("attacker-popup", allowed).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user