mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-22 11:23:59 +02:00
Refuse private ranges unconditionally; the proxy's network is never the user's
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -22,18 +22,6 @@ pub struct Config {
|
||||
)]
|
||||
pub allowed_origins: Vec<String>,
|
||||
|
||||
/// 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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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<AppState>) -> 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,
|
||||
}))
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user