From 4821970ec5699f1d40d3b28909005882439887a1 Mon Sep 17 00:00:00 2001 From: Ryan Levick Date: Fri, 6 Sep 2024 14:43:20 +0200 Subject: [PATCH] Give outbound http ability to ban private ips Signed-off-by: Ryan Levick --- Cargo.lock | 7 +++ crates/factor-outbound-http/Cargo.toml | 3 +- crates/factor-outbound-http/src/lib.rs | 20 +++++-- crates/factor-outbound-http/src/wasi.rs | 44 ++++++++++------ .../factor-outbound-http/tests/factor_test.rs | 2 +- crates/runtime-factors/src/lib.rs | 2 +- examples/spin-timer/Cargo.lock | 52 +++++++++++-------- 7 files changed, 84 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4ceb7fd44..42be97e2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3632,6 +3632,12 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + [[package]] name = "ipnet" version = "2.9.0" @@ -7307,6 +7313,7 @@ dependencies = [ "http 1.1.0", "http-body-util", "hyper 1.4.1", + "ip_network", "reqwest 0.11.27", "rustls 0.23.7", "spin-factor-outbound-networking", diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml index 9d22b59084..f65c39eb63 100644 --- a/crates/factor-outbound-http/Cargo.toml +++ b/crates/factor-outbound-http/Cargo.toml @@ -9,6 +9,7 @@ anyhow = "1.0" http = "1.1.0" http-body-util = "0.1" hyper = "1.4.1" +ip_network = "0.4" reqwest = { version = "0.11", features = ["gzip"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } spin-factor-outbound-networking = { path = "../factor-outbound-networking" } @@ -16,7 +17,7 @@ spin-factors = { path = "../factors" } spin-telemetry = { path = "../telemetry" } spin-world = { path = "../world" } terminal = { path = "../terminal" } -tokio = { version = "1", features = ["macros", "rt"] } +tokio = { version = "1", features = ["macros", "rt", "net"] } tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] } tracing = { workspace = true } wasmtime = { workspace = true } diff --git a/crates/factor-outbound-http/src/lib.rs b/crates/factor-outbound-http/src/lib.rs index 1e6f6c53f9..fa39de13dc 100644 --- a/crates/factor-outbound-http/src/lib.rs +++ b/crates/factor-outbound-http/src/lib.rs @@ -24,14 +24,24 @@ pub use wasmtime_wasi_http::{ HttpResult, }; -#[derive(Default)] pub struct OutboundHttpFactor { - _priv: (), + allow_private_ips: bool, } impl OutboundHttpFactor { - pub fn new() -> Self { - Self::default() + /// Create a new OutboundHttpFactor. + /// + /// If `allow_private_ips` is true, requests to private IP addresses will be allowed. + pub fn new(allow_private_ips: bool) -> Self { + Self { allow_private_ips } + } +} + +impl Default for OutboundHttpFactor { + fn default() -> Self { + Self { + allow_private_ips: true, + } } } @@ -66,6 +76,7 @@ impl Factor for OutboundHttpFactor { Ok(InstanceState { wasi_http_ctx: WasiHttpCtx::new(), allowed_hosts, + allow_private_ips: self.allow_private_ips, component_tls_configs, self_request_origin: None, request_interceptor: None, @@ -77,6 +88,7 @@ impl Factor for OutboundHttpFactor { pub struct InstanceState { wasi_http_ctx: WasiHttpCtx, allowed_hosts: OutboundAllowedHosts, + allow_private_ips: bool, component_tls_configs: ComponentTlsConfigs, self_request_origin: Option, request_interceptor: Option>, diff --git a/crates/factor-outbound-http/src/wasi.rs b/crates/factor-outbound-http/src/wasi.rs index 8d49bad2ab..0b0f2c29f1 100644 --- a/crates/factor-outbound-http/src/wasi.rs +++ b/crates/factor-outbound-http/src/wasi.rs @@ -1,8 +1,9 @@ -use std::{error::Error, sync::Arc}; +use std::{error::Error, net::IpAddr, sync::Arc}; use anyhow::Context; use http::{header::HOST, Request}; use http_body_util::BodyExt; +use ip_network::IpNetwork; use rustls::ClientConfig; use spin_factor_outbound_networking::OutboundAllowedHosts; use spin_factors::{wasmtime::component::ResourceTable, RuntimeFactorsInstanceState}; @@ -123,6 +124,7 @@ impl<'a> WasiHttpView for WasiHttpImplInner<'a> { self.state.allowed_hosts.clone(), self.state.self_request_origin.clone(), tls_client_config, + self.state.allow_private_ips, ) .in_current_span(), ), @@ -136,6 +138,7 @@ async fn send_request_impl( outbound_allowed_hosts: OutboundAllowedHosts, self_request_origin: Option, tls_client_config: Arc, + allow_private_ips: bool, ) -> anyhow::Result> { if request.uri().authority().is_some() { // Absolute URI @@ -177,7 +180,7 @@ async fn send_request_impl( current_span.record("server.port", port.as_u16()); } - Ok(send_request_handler(request, config, tls_client_config).await) + Ok(send_request_handler(request, config, tls_client_config, allow_private_ips).await) } /// This is a fork of wasmtime_wasi_http::default_send_request_handler function @@ -192,6 +195,7 @@ async fn send_request_handler( between_bytes_timeout, }: wasmtime_wasi_http::types::OutgoingRequestConfig, tls_client_config: Arc, + allow_private_ips: bool, ) -> Result { let authority_str = if let Some(authority) = request.uri().authority() { if authority.port().is_some() { @@ -204,23 +208,26 @@ async fn send_request_handler( return Err(ErrorCode::HttpRequestUriInvalid); }; - let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority_str)) + // Resolve the authority to IP addresses + let mut socket_addrs = tokio::net::lookup_host(&authority_str) + .await + .map_err(|_| dns_error("address not available".into(), 0))? + .collect::>(); + + // Potentially filter out private IPs + if !allow_private_ips && !socket_addrs.is_empty() { + socket_addrs.retain(|addr| !is_private_ip(addr.ip())); + if socket_addrs.is_empty() { + return Err(ErrorCode::DestinationIpProhibited); + } + } + + let tcp_stream = timeout(connect_timeout, TcpStream::connect(socket_addrs.as_slice())) .await .map_err(|_| ErrorCode::ConnectionTimeout)? .map_err(|err| match err.kind() { - std::io::ErrorKind::AddrNotAvailable => { - dns_error("address not available".to_string(), 0) - } - _ => { - if err - .to_string() - .starts_with("failed to lookup address information") - { - dns_error("address not available".to_string(), 0) - } else { - ErrorCode::ConnectionRefused - } - } + std::io::ErrorKind::AddrNotAvailable => dns_error("address not available".into(), 0), + _ => ErrorCode::ConnectionRefused, })?; let (mut sender, worker) = if use_tls { @@ -337,3 +344,8 @@ fn dns_error(rcode: String, info_code: u16) -> ErrorCode { info_code: Some(info_code), }) } + +/// Returns true if the IP is a private IP address. +fn is_private_ip(ip: IpAddr) -> bool { + !IpNetwork::from(ip).is_global() +} diff --git a/crates/factor-outbound-http/tests/factor_test.rs b/crates/factor-outbound-http/tests/factor_test.rs index 42bfe7c5ac..2ec4d9e801 100644 --- a/crates/factor-outbound-http/tests/factor_test.rs +++ b/crates/factor-outbound-http/tests/factor_test.rs @@ -77,7 +77,7 @@ async fn test_instance_state( let factors = TestFactors { variables: VariablesFactor::default(), networking: OutboundNetworkingFactor::new(), - http: OutboundHttpFactor::new(), + http: OutboundHttpFactor::default(), }; let env = TestEnvironment::new(factors).extend_manifest(toml! { [component.test-component] diff --git a/crates/runtime-factors/src/lib.rs b/crates/runtime-factors/src/lib.rs index 276c3c8360..95174ed3c0 100644 --- a/crates/runtime-factors/src/lib.rs +++ b/crates/runtime-factors/src/lib.rs @@ -49,7 +49,7 @@ impl TriggerFactors { variables: VariablesFactor::default(), key_value: KeyValueFactor::new(default_key_value_label_resolver), outbound_networking: outbound_networking_factor(), - outbound_http: OutboundHttpFactor::new(), + outbound_http: OutboundHttpFactor::default(), sqlite: SqliteFactor::new(default_sqlite_label_resolver), redis: OutboundRedisFactor::new(), mqtt: OutboundMqttFactor::new(NetworkedMqttClient::creator()), diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index 455aeda1c1..012bd3298e 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -1903,6 +1903,12 @@ version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + [[package]] name = "ipnet" version = "2.9.0" @@ -1955,9 +1961,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1996,7 +2002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2449,9 +2455,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -3813,6 +3819,7 @@ dependencies = [ "http 1.1.0", "http-body-util", "hyper 1.4.1", + "ip_network", "reqwest 0.11.27", "rustls 0.23.12", "spin-factor-outbound-networking", @@ -5064,20 +5071,19 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", - "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -5090,9 +5096,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -5102,9 +5108,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5112,9 +5118,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -5125,9 +5131,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-encoder" @@ -5613,9 +5619,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -5717,11 +5723,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "windows-sys 0.52.0", + "winapi", ] [[package]]