Skip to content

Commit 4821970

Browse files
committed
Give outbound http ability to ban private ips
Signed-off-by: Ryan Levick <[email protected]>
1 parent 57b1e54 commit 4821970

File tree

7 files changed

+84
-46
lines changed

7 files changed

+84
-46
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/factor-outbound-http/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ anyhow = "1.0"
99
http = "1.1.0"
1010
http-body-util = "0.1"
1111
hyper = "1.4.1"
12+
ip_network = "0.4"
1213
reqwest = { version = "0.11", features = ["gzip"] }
1314
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
1415
spin-factor-outbound-networking = { path = "../factor-outbound-networking" }
1516
spin-factors = { path = "../factors" }
1617
spin-telemetry = { path = "../telemetry" }
1718
spin-world = { path = "../world" }
1819
terminal = { path = "../terminal" }
19-
tokio = { version = "1", features = ["macros", "rt"] }
20+
tokio = { version = "1", features = ["macros", "rt", "net"] }
2021
tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"] }
2122
tracing = { workspace = true }
2223
wasmtime = { workspace = true }

crates/factor-outbound-http/src/lib.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,24 @@ pub use wasmtime_wasi_http::{
2424
HttpResult,
2525
};
2626

27-
#[derive(Default)]
2827
pub struct OutboundHttpFactor {
29-
_priv: (),
28+
allow_private_ips: bool,
3029
}
3130

3231
impl OutboundHttpFactor {
33-
pub fn new() -> Self {
34-
Self::default()
32+
/// Create a new OutboundHttpFactor.
33+
///
34+
/// If `allow_private_ips` is true, requests to private IP addresses will be allowed.
35+
pub fn new(allow_private_ips: bool) -> Self {
36+
Self { allow_private_ips }
37+
}
38+
}
39+
40+
impl Default for OutboundHttpFactor {
41+
fn default() -> Self {
42+
Self {
43+
allow_private_ips: true,
44+
}
3545
}
3646
}
3747

@@ -66,6 +76,7 @@ impl Factor for OutboundHttpFactor {
6676
Ok(InstanceState {
6777
wasi_http_ctx: WasiHttpCtx::new(),
6878
allowed_hosts,
79+
allow_private_ips: self.allow_private_ips,
6980
component_tls_configs,
7081
self_request_origin: None,
7182
request_interceptor: None,
@@ -77,6 +88,7 @@ impl Factor for OutboundHttpFactor {
7788
pub struct InstanceState {
7889
wasi_http_ctx: WasiHttpCtx,
7990
allowed_hosts: OutboundAllowedHosts,
91+
allow_private_ips: bool,
8092
component_tls_configs: ComponentTlsConfigs,
8193
self_request_origin: Option<SelfRequestOrigin>,
8294
request_interceptor: Option<Box<dyn OutboundHttpInterceptor>>,

crates/factor-outbound-http/src/wasi.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use std::{error::Error, sync::Arc};
1+
use std::{error::Error, net::IpAddr, sync::Arc};
22

33
use anyhow::Context;
44
use http::{header::HOST, Request};
55
use http_body_util::BodyExt;
6+
use ip_network::IpNetwork;
67
use rustls::ClientConfig;
78
use spin_factor_outbound_networking::OutboundAllowedHosts;
89
use spin_factors::{wasmtime::component::ResourceTable, RuntimeFactorsInstanceState};
@@ -123,6 +124,7 @@ impl<'a> WasiHttpView for WasiHttpImplInner<'a> {
123124
self.state.allowed_hosts.clone(),
124125
self.state.self_request_origin.clone(),
125126
tls_client_config,
127+
self.state.allow_private_ips,
126128
)
127129
.in_current_span(),
128130
),
@@ -136,6 +138,7 @@ async fn send_request_impl(
136138
outbound_allowed_hosts: OutboundAllowedHosts,
137139
self_request_origin: Option<SelfRequestOrigin>,
138140
tls_client_config: Arc<ClientConfig>,
141+
allow_private_ips: bool,
139142
) -> anyhow::Result<Result<IncomingResponse, ErrorCode>> {
140143
if request.uri().authority().is_some() {
141144
// Absolute URI
@@ -177,7 +180,7 @@ async fn send_request_impl(
177180
current_span.record("server.port", port.as_u16());
178181
}
179182

180-
Ok(send_request_handler(request, config, tls_client_config).await)
183+
Ok(send_request_handler(request, config, tls_client_config, allow_private_ips).await)
181184
}
182185

183186
/// This is a fork of wasmtime_wasi_http::default_send_request_handler function
@@ -192,6 +195,7 @@ async fn send_request_handler(
192195
between_bytes_timeout,
193196
}: wasmtime_wasi_http::types::OutgoingRequestConfig,
194197
tls_client_config: Arc<ClientConfig>,
198+
allow_private_ips: bool,
195199
) -> Result<wasmtime_wasi_http::types::IncomingResponse, ErrorCode> {
196200
let authority_str = if let Some(authority) = request.uri().authority() {
197201
if authority.port().is_some() {
@@ -204,23 +208,26 @@ async fn send_request_handler(
204208
return Err(ErrorCode::HttpRequestUriInvalid);
205209
};
206210

207-
let tcp_stream = timeout(connect_timeout, TcpStream::connect(&authority_str))
211+
// Resolve the authority to IP addresses
212+
let mut socket_addrs = tokio::net::lookup_host(&authority_str)
213+
.await
214+
.map_err(|_| dns_error("address not available".into(), 0))?
215+
.collect::<Vec<_>>();
216+
217+
// Potentially filter out private IPs
218+
if !allow_private_ips && !socket_addrs.is_empty() {
219+
socket_addrs.retain(|addr| !is_private_ip(addr.ip()));
220+
if socket_addrs.is_empty() {
221+
return Err(ErrorCode::DestinationIpProhibited);
222+
}
223+
}
224+
225+
let tcp_stream = timeout(connect_timeout, TcpStream::connect(socket_addrs.as_slice()))
208226
.await
209227
.map_err(|_| ErrorCode::ConnectionTimeout)?
210228
.map_err(|err| match err.kind() {
211-
std::io::ErrorKind::AddrNotAvailable => {
212-
dns_error("address not available".to_string(), 0)
213-
}
214-
_ => {
215-
if err
216-
.to_string()
217-
.starts_with("failed to lookup address information")
218-
{
219-
dns_error("address not available".to_string(), 0)
220-
} else {
221-
ErrorCode::ConnectionRefused
222-
}
223-
}
229+
std::io::ErrorKind::AddrNotAvailable => dns_error("address not available".into(), 0),
230+
_ => ErrorCode::ConnectionRefused,
224231
})?;
225232

226233
let (mut sender, worker) = if use_tls {
@@ -337,3 +344,8 @@ fn dns_error(rcode: String, info_code: u16) -> ErrorCode {
337344
info_code: Some(info_code),
338345
})
339346
}
347+
348+
/// Returns true if the IP is a private IP address.
349+
fn is_private_ip(ip: IpAddr) -> bool {
350+
!IpNetwork::from(ip).is_global()
351+
}

crates/factor-outbound-http/tests/factor_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ async fn test_instance_state(
7777
let factors = TestFactors {
7878
variables: VariablesFactor::default(),
7979
networking: OutboundNetworkingFactor::new(),
80-
http: OutboundHttpFactor::new(),
80+
http: OutboundHttpFactor::default(),
8181
};
8282
let env = TestEnvironment::new(factors).extend_manifest(toml! {
8383
[component.test-component]

crates/runtime-factors/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl TriggerFactors {
4949
variables: VariablesFactor::default(),
5050
key_value: KeyValueFactor::new(default_key_value_label_resolver),
5151
outbound_networking: outbound_networking_factor(),
52-
outbound_http: OutboundHttpFactor::new(),
52+
outbound_http: OutboundHttpFactor::default(),
5353
sqlite: SqliteFactor::new(default_sqlite_label_resolver),
5454
redis: OutboundRedisFactor::new(),
5555
mqtt: OutboundMqttFactor::new(NetworkedMqttClient::creator()),

examples/spin-timer/Cargo.lock

Lines changed: 29 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)