diff --git a/src-tauri/src/sync2/http.rs b/src-tauri/src/sync2/http.rs index c1a9c76a..76dac961 100644 --- a/src-tauri/src/sync2/http.rs +++ b/src-tauri/src/sync2/http.rs @@ -2,7 +2,7 @@ //! anywhere, plain HTTP only to a private address or a `.local` name, no redirects, and bounded //! responses (Untrusted input). -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::time::Duration; use reqwest::{Client, ClientBuilder, Response, Url, redirect}; @@ -25,6 +25,16 @@ pub async fn read_body(mut response: Response, limit: usize) -> Result> Ok(body) } +/// `is_local_ip` leaves out IPv6 link-local for QR codes, which can't carry a zone id; a store's +/// address can reach one, and `.local` names often resolve to one. +fn private_address(ip: IpAddr) -> bool { + is_local_ip(ip) || matches!(ip, IpAddr::V6(v6) if link_local(v6)) +} + +fn link_local(ip: Ipv6Addr) -> bool { + ip.segments()[0] & 0xffc0 == 0xfe80 +} + pub struct Transport { /// A `.local` name over plain HTTP, checked and pinned so DNS can't move it afterwards. pinned: Option<(String, SocketAddr)>, @@ -48,7 +58,7 @@ impl Transport { .host_str() .ok_or(StoreError::Insecure)? .trim_matches(['[', ']']); - if host.parse::().is_ok_and(is_local_ip) { + if host.parse::().is_ok_and(private_address) { return Ok(Self { pinned: None, direct: true, @@ -63,7 +73,7 @@ impl Transport { .map_err(|_| StoreError::Insecure)? .collect(); match addresses.first() { - Some(first) if addresses.iter().all(|a| is_local_ip(a.ip())) => Ok(Self { + Some(first) if addresses.iter().all(|a| private_address(a.ip())) => Ok(Self { pinned: Some((host.to_owned(), *first)), direct: true, }), diff --git a/src-tauri/tests/sync2_server.rs b/src-tauri/tests/sync2_server.rs index d2038d06..a6c384b9 100644 --- a/src-tauri/tests/sync2_server.rs +++ b/src-tauri/tests/sync2_server.rs @@ -92,6 +92,8 @@ async fn plain_http_is_allowed_only_to_private_addresses() { assert!(!insecure("http://100.101.102.103/").await); assert!(!insecure("http://[::ffff:10.0.0.2]:8080/").await); assert!(!insecure("http://[fd12::1]/").await); + assert!(!insecure("http://[fe80::1]/").await); + assert!(insecure("http://[fec0::1]/").await); assert!(insecure("http://203.0.113.5/").await); assert!(insecure("http://sync.example.org/").await); assert!(insecure("http://[::ffff:203.0.113.5]/").await);