diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index c77b612f7..e345ee0ee 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -53,6 +53,15 @@ impl Remote { self.conn = None; let mut last = std::io::Error::new(std::io::ErrorKind::NotFound, format!("cannot resolve {}", self.addr)); for sa in self.addr.to_socket_addrs()? { + // The token goes over plain TCP, so only to this machine: a network peer could read it, + // and the app's control port listens on loopback only. + if !sa.ip().is_loopback() { + last = std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + format!("{sa} is not a loopback address: the control token is sent only to 127.0.0.1 or ::1"), + ); + continue; + } match TcpStream::connect_timeout(&sa, Duration::from_secs(2)) { Ok(s) => { s.set_nodelay(true).ok(); diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 5a5297246..0f9dd665f 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -482,6 +482,9 @@ fn remote_backend_over_tcp() { drop(s); handle.join().unwrap(); assert!(Remote::connect_with_token("127.0.0.1:1", "x".into()).is_err()); + // A network address is refused before anything, the token included, is sent. + let err = Remote::connect_with_token("192.0.2.1:7979", "x".into()).err().unwrap(); + assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied, "{err}"); } #[test]