diff --git a/crates/zotero/src/client.rs b/crates/zotero/src/client.rs index e7a73354e..f6e31c83a 100644 --- a/crates/zotero/src/client.rs +++ b/crates/zotero/src/client.rs @@ -90,7 +90,8 @@ pub struct Outcome { pub fn run_command(opts: &Options, command: Command, answer: &mut dyn FnMut(&Call) -> Result) -> Result { crate::peer::check_addr(&opts.addr)?; let mut stream = TcpStream::connect_timeout(&opts.addr, opts.connect_timeout).map_err(|e| ZoteroError::NotRunning(e.to_string()))?; - // Before Zotero sees anything: the command, and then the document, go only to our own user. + // Before Zotero sees anything: the command, and then the document, go only to this computer + // and, on Linux only, not to a listener the socket table shows belongs to another user. crate::peer::check(&stream)?; let _ = stream.set_nodelay(true); stream.set_read_timeout(Some(opts.idle_timeout)).map_err(|e| ZoteroError::Io(e.to_string()))?; diff --git a/crates/zotero/src/lib.rs b/crates/zotero/src/lib.rs index 7dc26eb8e..cf4c93443 100644 --- a/crates/zotero/src/lib.rs +++ b/crates/zotero/src/lib.rs @@ -38,8 +38,8 @@ pub enum ZoteroError { /// Zotero sent something that isn't the protocol. #[error("unexpected data from Zotero: {0}")] Protocol(String), - /// What answered on Zotero's port may not be Zotero (another user's program, or another - /// computer); it gets no access to the document. + /// What answered on Zotero's port may not be Zotero (another computer, on any system; on + /// Linux only, another user's program); it gets no access to the document. #[error("WordCraft won't let this program edit the document: {0}")] Refused(String), } diff --git a/crates/zotero/src/peer.rs b/crates/zotero/src/peer.rs index 8e6ea6c4e..07df98e79 100644 --- a/crates/zotero/src/peer.rs +++ b/crates/zotero/src/peer.rs @@ -1,11 +1,16 @@ //! Who answered on Zotero's port. Zotero's protocol has no handshake that proves the other end -//! is Zotero, and whatever answers gets to read and rewrite the open document. So WordCraft -//! only connects to this computer (a loopback address), and on Linux checks that the program -//! listening there runs as the same user, from the kernel's socket table (`/proc/net/tcp`): -//! another account on a shared machine that took the port while Zotero wasn't running is -//! refused. A listener the table doesn't show (WSL2 mirrored networking with Zotero on Windows, -//! a local proxy, a sandbox) leaves only the loopback rule, as on other systems, which have no -//! such table to read without OS calls this crate doesn't make (see `docs/zotero.md`). +//! is Zotero, and whatever answers gets to read and rewrite the open document. Two checks: +//! +//! - **Every system:** WordCraft only connects to this computer (a loopback address). +//! - **Linux only:** the program listening there must not run as another user, from the +//! kernel's socket table (`/proc/net/tcp`): another account on a shared machine that took the +//! port while Zotero wasn't running is refused. A listener the table doesn't show (WSL2 +//! mirrored networking with Zotero on Windows, a local proxy, a sandbox) leaves only the +//! loopback rule. +//! +//! On Windows, macOS and the BSDs there is **no same-user check**: the loopback rule is the +//! only one, so any local account's listener on the port is taken for Zotero. Those systems have +//! no such table to read without OS calls this crate doesn't make (see `docs/zotero.md`, #3831). use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream}; @@ -49,7 +54,8 @@ fn owner_check(local: SocketAddr, peer: SocketAddr) -> Result<(), ZoteroError> { same_owner(&table, local, peer, me) } -/// No socket table to read here: the loopback rule alone decides. +/// Windows, macOS, the BSDs: no same-user check (no socket table to read without OS calls this +/// crate doesn't make, #3831); the loopback rule alone decides. #[cfg(not(any(target_os = "linux", target_os = "android")))] fn owner_check(_local: SocketAddr, _peer: SocketAddr) -> Result<(), ZoteroError> { Ok(()) diff --git a/docs/zotero.md b/docs/zotero.md index 601749d59..cf5813494 100644 --- a/docs/zotero.md +++ b/docs/zotero.md @@ -64,22 +64,25 @@ Word's "Field" in the file.) ## Who can drive the document Zotero's protocol has no handshake that proves the program on port 23116 is Zotero, and that -program can read the citations and edit the open document. WordCraft limits who that can be: +program can read the citations and edit the open document. WordCraft limits who that can be. +In short: **every system** gets the loopback rule; **only Linux** also checks that the listener +isn't another user's. On Windows and macOS (and the BSDs) the loopback rule is the only check. -- It only connects to this computer (a loopback address such as `127.0.0.1`); any other address - is refused before connecting. -- On Linux it checks, in the kernel's socket table (`/proc/net/tcp`, `/proc/net/tcp6`), that the +- Every system: it only connects to this computer (a loopback address such as `127.0.0.1`); + any other address is refused before connecting. +- Linux only: it checks, in the kernel's socket table (`/proc/net/tcp`, `/proc/net/tcp6`), that the program listening on the port runs as the same user as WordCraft, before sending anything. If another account on a shared machine took the port while Zotero wasn't running, WordCraft refuses with an error and the document isn't touched. When the table doesn't show the listener (WSL2 mirrored networking with Zotero running on Windows, a local proxy, a sandbox) or can't be read, its owner is unknown and only the loopback rule applies, as below. -- Remaining risk: macOS, Windows and the BSDs have no such table to read without OS calls - WordCraft doesn't make (it uses no `unsafe` code), so there only the loopback rule applies - (and on Linux when the listener isn't in the table): on a computer shared with other - accounts, a program another user leaves on port 23116 while Zotero is closed would be taken - for Zotero. A program running as you is trusted on every - system; it could change your documents anyway. +- Windows, macOS and the BSDs: **no same-user check.** They have no such table to read + without OS calls WordCraft doesn't make (it uses no `unsafe` code), so only the loopback rule + applies there (and on Linux when the listener isn't in the table). Remaining risk: on a + computer shared with other accounts, a program another user leaves on port 23116 while + Zotero is closed would be taken for Zotero. An owner check for these systems is tracked in + #3831. A program running as you is trusted on every system; it could change your documents + anyway. - `wordcraft-cli zotero` prints Zotero's messages (and `--trace` lines) with control characters, line breaks and backslashes shown as escapes (`\n`, `\u{1b}`, `\\`), so a peer can't forge other output or drive the terminal.