mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 20:07:19 -06:00
#3692: Zotero owner check refuses only another user's listener; unlisted falls back to loopback
Ultrareview finding 3: on Linux, when /proc/net/tcp(6) doesn't list the listener on Zotero's port (WSL2 mirrored networking with Zotero on Windows, a local proxy, a sandbox), every connection was refused, so Zotero couldn't be used at all there. Per the owner's decision, refuse only when both sockets are found and the listener's uid differs from ours; otherwise log at debug and apply the plain loopback rule, as on macOS/Windows/BSD. Test: an_unlisted_listener_falls_back_to_the_loopback_rule. docs/zotero.md and the progress-log row say so. Non-Linux owner check stays with #3831. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9cc06dc0f
commit
7ee101a5b9
+21
-10
@@ -3,8 +3,9 @@
|
||||
//! 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. Other systems have no such table to read without OS calls this crate doesn't make;
|
||||
//! there the loopback rule is the guard (see `docs/zotero.md`).
|
||||
//! 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`).
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream};
|
||||
|
||||
@@ -50,9 +51,9 @@ fn owner_check(_local: SocketAddr, _peer: SocketAddr) -> Result<(), ZoteroError>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// From a `/proc/net/tcp`-format socket table: whether the socket at `peer` connected to
|
||||
/// `local` (the listener's side) belongs to the same user as ours. A peer that can't be found
|
||||
/// is refused too: its owner can't be established.
|
||||
/// From a `/proc/net/tcp`-format socket table: refuse when the socket at `peer` connected to
|
||||
/// `local` (the listener's side) belongs to another user than ours. When either socket isn't
|
||||
/// listed (or the table couldn't be read) the owner is unknown and the loopback rule decides.
|
||||
#[cfg_attr(not(any(target_os = "linux", target_os = "android")), allow(dead_code))]
|
||||
pub(crate) fn same_owner(table: &str, local: SocketAddr, peer: SocketAddr) -> Result<(), ZoteroError> {
|
||||
let key = |a: SocketAddr| (a.ip().to_canonical(), a.port());
|
||||
@@ -76,7 +77,10 @@ pub(crate) fn same_owner(table: &str, local: SocketAddr, peer: SocketAddr) -> Re
|
||||
(Some(_), Some(b)) => {
|
||||
Err(ZoteroError::Refused(format!("the program on port {} runs as another user (uid {b}), so it isn't your Zotero", peer.1)))
|
||||
}
|
||||
_ => Err(ZoteroError::Refused(format!("WordCraft couldn't tell which user's program is on port {}", peer.1))),
|
||||
_ => {
|
||||
log::debug!("Zotero: port {} isn't in this system's socket table; trusting the loopback address alone", peer.1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,15 +146,22 @@ mod tests {
|
||||
assert_eq!(same_owner(&same, local, peer), Ok(()));
|
||||
let other = format!("{HEADER}{}{}", row(local, peer, 1000), row(peer, local, 1001));
|
||||
assert!(matches!(same_owner(&other, local, peer), Err(ZoteroError::Refused(e)) if e.contains("another user")));
|
||||
// Not found (or the table couldn't be read): the owner can't be established.
|
||||
let missing = format!("{HEADER}{}", row(local, peer, 1000));
|
||||
assert!(matches!(same_owner(&missing, local, peer), Err(ZoteroError::Refused(_))));
|
||||
assert!(matches!(same_owner("", local, peer), Err(ZoteroError::Refused(_))));
|
||||
// Junk lines are skipped.
|
||||
let junk = format!("{HEADER}garbage\n 1: zz:zz yy:yy 01 x x x x\n{}{}", row(local, peer, 7), row(peer, local, 7));
|
||||
assert_eq!(same_owner(&junk, local, peer), Ok(()));
|
||||
}
|
||||
|
||||
/// WSL2 mirrored networking (Zotero on Windows), a local proxy or a sandbox: the listener
|
||||
/// isn't in our table, so the loopback rule decides, as on other systems.
|
||||
#[test]
|
||||
fn an_unlisted_listener_falls_back_to_the_loopback_rule() {
|
||||
let (local, peer) = addrs();
|
||||
let missing = format!("{HEADER}{}", row(local, peer, 1000));
|
||||
assert_eq!(same_owner(&missing, local, peer), Ok(()));
|
||||
// The table couldn't be read.
|
||||
assert_eq!(same_owner("", local, peer), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dual_stack_listener_is_matched_through_its_mapped_address() {
|
||||
let (local, peer) = addrs();
|
||||
|
||||
+8
-6
@@ -70,13 +70,15 @@ program can read the citations and edit the open document. WordCraft limits who
|
||||
is refused before connecting.
|
||||
- On Linux 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, or the owner
|
||||
can't be established (the table can't be read), WordCraft refuses with an error and the
|
||||
document isn't touched.
|
||||
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:
|
||||
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
|
||||
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.
|
||||
- `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
|
||||
|
||||
Reference in New Issue
Block a user