From 679e49d1eb1d5a5f2f8f3a6b48a1e270b1454b34 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 17 Aug 2026 11:00:39 -0700 Subject: [PATCH] Refuse private ranges unconditionally; the proxy's network is never the user's --- crates-server/yaak-send-proxy/README.md | 9 ++++--- crates-server/yaak-send-proxy/src/config.rs | 12 ---------- crates-server/yaak-send-proxy/src/guard.rs | 26 +++++++-------------- crates-server/yaak-send-proxy/src/main.rs | 6 ++--- packages/platform/src/web/README.md | 4 +++- 5 files changed, 17 insertions(+), 40 deletions(-) diff --git a/crates-server/yaak-send-proxy/README.md b/crates-server/yaak-send-proxy/README.md index 81ab1c6d..123cc685 100644 --- a/crates-server/yaak-send-proxy/README.md +++ b/crates-server/yaak-send-proxy/README.md @@ -35,7 +35,6 @@ arguments; `--help` lists them all. | --- | --- | --- | | `--bind` | `127.0.0.1:9227` | Listen address. `0.0.0.0:9227` inside a container. | | `--allowed-origins` | `*` | CORS origins, comma-separated. A hosted instance should name its web origin. | -| `--allow-private-networks` | off | Let sends reach private, loopback and link-local addresses. **Off by default; see below.** | | `--max-request-bytes` | 16 MiB | Largest rendered request accepted from the tab. | | `--max-response-bytes` | 64 MiB | Largest upstream body relayed before the send is cut off. | | `--max-timeout-secs` | 60 | Ceiling on a send's timeout; a request asking for more (or none) gets this. | @@ -47,7 +46,7 @@ arguments; `--help` lists them all. A hosted proxy is, by construction, a machine that makes HTTP requests on behalf of strangers. Left alone that is an open relay into whatever network it -sits on. So by default it refuses to connect to: +sits on. So it refuses, always, to connect to: - loopback (`127/8`, `::1`), private (`10/8`, `172.16/12`, `192.168/16`, `fc00::/7`), link-local (`169.254/16` — where cloud metadata lives — and @@ -62,9 +61,9 @@ caught, and so is a `Location:` header that points at one. It also refuses body types that would read files on the proxy's disk (`binary`, multipart file fields), since no browser tab could legitimately mean those. -Refusals are logged with the reason. A self-hosted instance on a private network -that legitimately needs to reach the services next to it turns the range check -off with `--allow-private-networks`. +Refusals are logged with the reason. There is no switch to turn this off: the +proxy's private network is the cloud's, not the user's, so a `localhost` or LAN +API can never be reached through it — that is what the desktop app is for. ## Self-hosting diff --git a/crates-server/yaak-send-proxy/src/config.rs b/crates-server/yaak-send-proxy/src/config.rs index fe3880ae..91ae2ccd 100644 --- a/crates-server/yaak-send-proxy/src/config.rs +++ b/crates-server/yaak-send-proxy/src/config.rs @@ -22,18 +22,6 @@ pub struct Config { )] pub allowed_origins: Vec, - /// Allow sends to private, loopback, link-local and other non-public addresses. - /// - /// Off by default: a hosted instance must not become a relay into its own network. A - /// self-hosted instance on a private network legitimately needs this on to reach the - /// services next to it. - #[arg( - long, - env = "YAAK_PROXY_ALLOW_PRIVATE_NETWORKS", - default_value_t = false - )] - pub allow_private_networks: bool, - /// Largest request the proxy accepts from the tab (the rendered request JSON, body included). #[arg(long, env = "YAAK_PROXY_MAX_REQUEST_BYTES", default_value_t = 16 * 1024 * 1024)] pub max_request_bytes: usize, diff --git a/crates-server/yaak-send-proxy/src/guard.rs b/crates-server/yaak-send-proxy/src/guard.rs index d09177af..5ab3676d 100644 --- a/crates-server/yaak-send-proxy/src/guard.rs +++ b/crates-server/yaak-send-proxy/src/guard.rs @@ -20,17 +20,13 @@ use yaak_http::dns::AddressFilter; use yaak_http::sender::{HttpResponse, HttpResponseEvent, HttpSender}; use yaak_http::types::SendableHttpRequest; -/// The destination policy, built once from config and shared by every send. -#[derive(Clone)] -pub struct DestinationPolicy { - allow_private_networks: bool, -} +/// The destination policy, shared by every send: public addresses only, always. A hosted +/// proxy's "private network" is the cloud's, not the user's, so there is no configuration +/// that makes reaching it right. +#[derive(Clone, Default)] +pub struct DestinationPolicy; impl DestinationPolicy { - pub fn new(allow_private_networks: bool) -> Self { - Self { allow_private_networks } - } - /// Check a URL before a hop is attempted: scheme and literal IPs. A hostname that passes /// here still has its resolved addresses checked by [`Self::address_filter`]. pub fn check_url(&self, raw: &str) -> Result<(), String> { @@ -57,9 +53,6 @@ impl DestinationPolicy { } pub fn check_ip(&self, ip: IpAddr) -> Result<(), String> { - if self.allow_private_networks { - return Ok(()); - } match non_public_reason(ip) { Some(reason) => Err(format!( "Refusing to connect to {ip}: {reason}. This proxy only sends to public addresses" @@ -248,11 +241,8 @@ mod tests { } #[test] - fn private_networks_can_be_opted_in() { - let policy = DestinationPolicy::new(true); - assert!(policy.check_url("http://127.0.0.1/").is_ok()); - assert!(policy.check_url("http://169.254.169.254/").is_ok()); - let policy = DestinationPolicy::new(false); + fn literal_private_addresses_in_urls_are_refused() { + let policy = DestinationPolicy; assert!(policy.check_url("http://127.0.0.1/").is_err()); assert!(policy.check_url("http://[::1]/").is_err()); assert!(policy.check_url("http://169.254.169.254/latest/meta-data").is_err()); @@ -260,7 +250,7 @@ mod tests { #[test] fn only_http_schemes() { - let policy = DestinationPolicy::new(true); + let policy = DestinationPolicy; assert!(policy.check_url("ftp://example.com/").is_err()); assert!(policy.check_url("file:///etc/passwd").is_err()); assert!(policy.check_url("https://example.com/").is_ok()); diff --git a/crates-server/yaak-send-proxy/src/main.rs b/crates-server/yaak-send-proxy/src/main.rs index e35b749a..2641055a 100644 --- a/crates-server/yaak-send-proxy/src/main.rs +++ b/crates-server/yaak-send-proxy/src/main.rs @@ -49,7 +49,7 @@ async fn main() { env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); let config = Config::parse(); - let policy = DestinationPolicy::new(config.allow_private_networks); + let policy = DestinationPolicy; let state = AppState { limits: Arc::new(SendLimits { policy, @@ -81,8 +81,7 @@ async fn main() { std::process::exit(1); }); info!( - "yaak-send-proxy listening on http://{bind} (private networks: {}, rate limit: {}/min)", - if state.config.allow_private_networks { "allowed" } else { "refused" }, + "yaak-send-proxy listening on http://{bind} (rate limit: {}/min)", state.config.rate_limit_per_minute, ); @@ -108,7 +107,6 @@ async fn health(State(state): State) -> impl IntoResponse { Json(json!({ "ok": true, "version": env!("CARGO_PKG_VERSION"), - "privateNetworks": state.config.allow_private_networks, "maxResponseBytes": state.config.max_response_bytes, "maxTimeoutSecs": state.config.max_timeout_secs, })) diff --git a/packages/platform/src/web/README.md b/packages/platform/src/web/README.md index d292d8f3..667b20fd 100644 --- a/packages/platform/src/web/README.md +++ b/packages/platform/src/web/README.md @@ -208,7 +208,9 @@ template *function* (`${[ timestamp() ]}`) or an authentication plugin (bearer, basic, OAuth, …) is refused before anything leaves the tab, with a message naming what it needs; those light up when plugins run in the browser. Requests with a file body or multipart file fields are refused by the proxy (it has no access to -your files, and must not read its own). +your files, and must not read its own). And a request to `localhost` or a LAN +address can't work from a browser: the proxy runs elsewhere and refuses private +ranges outright — reaching your own machine's APIs is what the desktop app is for. The proxy URL is `VITE_YAAK_SEND_PROXY_URL` at build time, defaulting to `http://127.0.0.1:9227` (see `proxy.ts`). Run one with `cargo run -p yaak-send-proxy`.