Phase B.6 audit of commits7aee534(Trust-3/6 main-window guard + size cap on clipboard + paste surface),12b413d(broaden clipboard/paste allowlist to documented secondary windows), andf7af7b0(Trust-4 main- window guard on extract_content_tags_cmd). Existing coverage is strong: * commands/security.rs: 4 tests for ensure_main_window_label + ensure_window_in_set_label accept/reject paths. * commands/clipboard.rs: 3 size-cap tests via a shadow size_check helper. * commands/paste.rs: comprehensive — 4 backend-order, 4 clipboard-restore, 7 terminal-classification, 3 paste-size-cap, plus paste_cap_matches_clipboard_cap that pins the MAX_CLIPBOARD_BYTES == MAX_PASTE_BYTES invariant the commit explicitly cared about. * commands/llm.rs: extract_content_tags_cmd Trust-4 — unconditional ensure_main_window guard with no surface to test beyond what's there. One real residual. The12b413dcommit message states: "mirror the secondary-windows capability grant in src-tauri/capabilities/secondary-windows.json so the IPC trust boundary and the permission grant stay in lock-step." But no test pins the mirror invariant. A future change could: * add a new window to secondary-windows.json and forget to update CLIPBOARD_ALLOWED_WINDOWS or PASTE_REPLACING_ALLOWED_WINDOWS; * typo a label in one of the Rust consts; * remove a window from the JSON while leaving the const intact; * remove a window from the const while leaving the JSON intact. Each of those silently drifts the IPC trust boundary against the capability grant. The two halves stay in lock-step on intent — but the intent lives only in the commit message and a docstring, not in a runtime check. Fix (test-only, no production behaviour change): * Promote CLIPBOARD_ALLOWED_WINDOWS and PASTE_REPLACING_ALLOWED_WINDOWS from private to pub(crate) so a single shared test can reference them. * Cross-reference both consts in a new docstring back to the pinning test. * Add commands::security::tests_capability_mirror::allowlists_match_capability_jsons. The test reads capabilities/main.json + capabilities/secondary-windows.json at CARGO_MANIFEST_DIR, parses with serde_json, collects every label declared in the "windows" arrays, and asserts every label in both Rust allowlists is in that declared set. Asymmetric on purpose: the JSON may legitimately declare windows that don't need clipboard/paste (e.g. tasks-float doesn't), so the test does NOT assert const ⊇ JSON, only const ⊆ JSON. The over-restrict direction is safe; the under-restrict direction is the IPC bypass we care about. Verification: * cargo test -p lumotia --lib commands::security → 5/5 pass including the new allowlists_match_capability_jsons. * cargo fmt --check → clean (applied fmt after the test edit). * cargo clippy -p lumotia --all-targets -- -D warnings → clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
5.4 KiB
Rust
137 lines
5.4 KiB
Rust
pub fn ensure_main_window(window: &tauri::WebviewWindow) -> Result<(), String> {
|
|
ensure_main_window_label(window.label())
|
|
}
|
|
|
|
pub fn ensure_main_window_label(label: &str) -> Result<(), String> {
|
|
if label == "main" {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"This command is only available from the main window (got {label})."
|
|
))
|
|
}
|
|
}
|
|
|
|
/// 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.contains(&label) {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"This command is only available from {allowed:?} (got {label})."
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{ensure_main_window_label, ensure_window_in_set_label};
|
|
|
|
#[test]
|
|
fn accepts_main_window() {
|
|
assert!(ensure_main_window_label("main").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_secondary_windows() {
|
|
for label in ["tasks-float", "transcript-viewer", "transcription-preview"] {
|
|
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());
|
|
}
|
|
}
|
|
|
|
/// Pins the invariant flagged in the 12b413d commit message and the
|
|
/// Phase B.6 audit (2026-05-14): every label that the Rust IPC layer
|
|
/// allow-lists for clipboard / paste-replacing must also appear in one
|
|
/// of the Tauri capability JSONs' "windows" arrays. If the two halves
|
|
/// drift, the IPC trust boundary silently disagrees with the permission
|
|
/// grant — either the IPC layer permits a window the capability system
|
|
/// rejects (call dies with a Tauri permission error), or the capability
|
|
/// system permits a window the IPC layer rejects (call dies with the
|
|
/// Lumotia rejection message). Either is a maintenance footgun the
|
|
/// commit message claimed to address but did not actually pin.
|
|
#[cfg(test)]
|
|
mod tests_capability_mirror {
|
|
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
|
|
/// Read all `"windows"` labels declared across the capability JSONs
|
|
/// at `src-tauri/capabilities/`. The Tauri permission system uses
|
|
/// these arrays to scope capability grants per window; the Rust IPC
|
|
/// `ensure_window_in_set` callers must reference the same labels.
|
|
fn declared_window_labels() -> HashSet<String> {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let capabilities_dir = Path::new(manifest_dir).join("capabilities");
|
|
|
|
let mut labels: HashSet<String> = HashSet::new();
|
|
for filename in ["main.json", "secondary-windows.json"] {
|
|
let body = std::fs::read_to_string(capabilities_dir.join(filename))
|
|
.unwrap_or_else(|e| panic!("read {filename}: {e}"));
|
|
let json: serde_json::Value =
|
|
serde_json::from_str(&body).unwrap_or_else(|e| panic!("parse {filename}: {e}"));
|
|
let windows = json["windows"]
|
|
.as_array()
|
|
.unwrap_or_else(|| panic!("{filename} missing 'windows' array"));
|
|
for w in windows {
|
|
if let Some(label) = w.as_str() {
|
|
labels.insert(label.to_string());
|
|
}
|
|
}
|
|
}
|
|
labels
|
|
}
|
|
|
|
#[test]
|
|
fn allowlists_match_capability_jsons() {
|
|
let declared = declared_window_labels();
|
|
|
|
for label in crate::commands::clipboard::CLIPBOARD_ALLOWED_WINDOWS {
|
|
assert!(
|
|
declared.contains(*label),
|
|
"CLIPBOARD_ALLOWED_WINDOWS label {label:?} is not declared in any \
|
|
capabilities JSON ({}). Either remove the label from the Rust const \
|
|
or add the window to capabilities/secondary-windows.json so the IPC \
|
|
trust boundary and the capability grant stay in lock-step.",
|
|
declared.iter().cloned().collect::<Vec<_>>().join(", ")
|
|
);
|
|
}
|
|
|
|
for label in crate::commands::paste::PASTE_REPLACING_ALLOWED_WINDOWS {
|
|
assert!(
|
|
declared.contains(*label),
|
|
"PASTE_REPLACING_ALLOWED_WINDOWS label {label:?} is not declared in any \
|
|
capabilities JSON ({}). Either remove the label from the Rust const \
|
|
or add the window to capabilities/secondary-windows.json.",
|
|
declared.iter().cloned().collect::<Vec<_>>().join(", ")
|
|
);
|
|
}
|
|
}
|
|
}
|