mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-27 13:54:11 +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. |
|
| `--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. |
|
| `--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-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-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. |
|
| `--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
|
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
|
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`,
|
- 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
|
`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
|
types that would read files on the proxy's disk (`binary`, multipart file
|
||||||
fields), since no browser tab could legitimately mean those.
|
fields), since no browser tab could legitimately mean those.
|
||||||
|
|
||||||
Refusals are logged with the reason. A self-hosted instance on a private network
|
Refusals are logged with the reason. There is no switch to turn this off: the
|
||||||
that legitimately needs to reach the services next to it turns the range check
|
proxy's private network is the cloud's, not the user's, so a `localhost` or LAN
|
||||||
off with `--allow-private-networks`.
|
API can never be reached through it — that is what the desktop app is for.
|
||||||
|
|
||||||
## Self-hosting
|
## Self-hosting
|
||||||
|
|
||||||
|
|||||||
@@ -22,18 +22,6 @@ pub struct Config {
|
|||||||
)]
|
)]
|
||||||
pub allowed_origins: Vec<String>,
|
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).
|
/// 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)]
|
#[arg(long, env = "YAAK_PROXY_MAX_REQUEST_BYTES", default_value_t = 16 * 1024 * 1024)]
|
||||||
pub max_request_bytes: usize,
|
pub max_request_bytes: usize,
|
||||||
|
|||||||
@@ -20,17 +20,13 @@ use yaak_http::dns::AddressFilter;
|
|||||||
use yaak_http::sender::{HttpResponse, HttpResponseEvent, HttpSender};
|
use yaak_http::sender::{HttpResponse, HttpResponseEvent, HttpSender};
|
||||||
use yaak_http::types::SendableHttpRequest;
|
use yaak_http::types::SendableHttpRequest;
|
||||||
|
|
||||||
/// The destination policy, built once from config and shared by every send.
|
/// The destination policy, shared by every send: public addresses only, always. A hosted
|
||||||
#[derive(Clone)]
|
/// proxy's "private network" is the cloud's, not the user's, so there is no configuration
|
||||||
pub struct DestinationPolicy {
|
/// that makes reaching it right.
|
||||||
allow_private_networks: bool,
|
#[derive(Clone, Default)]
|
||||||
}
|
pub struct DestinationPolicy;
|
||||||
|
|
||||||
impl 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
|
/// 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`].
|
/// here still has its resolved addresses checked by [`Self::address_filter`].
|
||||||
pub fn check_url(&self, raw: &str) -> Result<(), String> {
|
pub fn check_url(&self, raw: &str) -> Result<(), String> {
|
||||||
@@ -57,9 +53,6 @@ impl DestinationPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_ip(&self, ip: IpAddr) -> Result<(), String> {
|
pub fn check_ip(&self, ip: IpAddr) -> Result<(), String> {
|
||||||
if self.allow_private_networks {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
match non_public_reason(ip) {
|
match non_public_reason(ip) {
|
||||||
Some(reason) => Err(format!(
|
Some(reason) => Err(format!(
|
||||||
"Refusing to connect to {ip}: {reason}. This proxy only sends to public addresses"
|
"Refusing to connect to {ip}: {reason}. This proxy only sends to public addresses"
|
||||||
@@ -248,11 +241,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn private_networks_can_be_opted_in() {
|
fn literal_private_addresses_in_urls_are_refused() {
|
||||||
let policy = DestinationPolicy::new(true);
|
let policy = DestinationPolicy;
|
||||||
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);
|
|
||||||
assert!(policy.check_url("http://127.0.0.1/").is_err());
|
assert!(policy.check_url("http://127.0.0.1/").is_err());
|
||||||
assert!(policy.check_url("http://[::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());
|
assert!(policy.check_url("http://169.254.169.254/latest/meta-data").is_err());
|
||||||
@@ -260,7 +250,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_http_schemes() {
|
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("ftp://example.com/").is_err());
|
||||||
assert!(policy.check_url("file:///etc/passwd").is_err());
|
assert!(policy.check_url("file:///etc/passwd").is_err());
|
||||||
assert!(policy.check_url("https://example.com/").is_ok());
|
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();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
let config = Config::parse();
|
let config = Config::parse();
|
||||||
|
|
||||||
let policy = DestinationPolicy::new(config.allow_private_networks);
|
let policy = DestinationPolicy;
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
limits: Arc::new(SendLimits {
|
limits: Arc::new(SendLimits {
|
||||||
policy,
|
policy,
|
||||||
@@ -81,8 +81,7 @@ async fn main() {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
});
|
});
|
||||||
info!(
|
info!(
|
||||||
"yaak-send-proxy listening on http://{bind} (private networks: {}, rate limit: {}/min)",
|
"yaak-send-proxy listening on http://{bind} (rate limit: {}/min)",
|
||||||
if state.config.allow_private_networks { "allowed" } else { "refused" },
|
|
||||||
state.config.rate_limit_per_minute,
|
state.config.rate_limit_per_minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,7 +107,6 @@ async fn health(State(state): State<AppState>) -> impl IntoResponse {
|
|||||||
Json(json!({
|
Json(json!({
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"version": env!("CARGO_PKG_VERSION"),
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
"privateNetworks": state.config.allow_private_networks,
|
|
||||||
"maxResponseBytes": state.config.max_response_bytes,
|
"maxResponseBytes": state.config.max_response_bytes,
|
||||||
"maxTimeoutSecs": state.config.max_timeout_secs,
|
"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
|
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
|
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
|
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
|
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`.
|
`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