#3692: review — say plainly the same-user Zotero check is Linux only; Windows and macOS rely on loopback alone

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
This commit is contained in:
Martin Ball
2026-10-25 23:49:15 +00:00
co-authored by Claude Opus 5.5
parent e251c960d9
commit bf2a379ec6
4 changed files with 31 additions and 21 deletions
+2 -1
View File
@@ -90,7 +90,8 @@ pub struct Outcome {
pub fn run_command(opts: &Options, command: Command, answer: &mut dyn FnMut(&Call) -> Result<Value, String>) -> Result<Outcome, ZoteroError> {
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()))?;
+2 -2
View File
@@ -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),
}
+14 -8
View File
@@ -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(())
+13 -10
View File
@@ -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.