CVE-2024-32980
Description
Spin is the developer tool for building and running serverless applications powered by WebAssembly. Prior to 2.4.3, some specifically configured Spin applications that use self requests without a specified URL authority can be induced to make requests to arbitrary hosts via the Host HTTP header. The following conditions need to be met for an application to be vulnerable: 1. The environment Spin is deployed in routes requests to the Spin runtime based on the request URL instead of the Host header, and leaves the Host header set to its original value; 2. The Spin application's component handling the incoming request is configured with an allow_outbound_hosts list containing "self"; and 3. In reaction to an incoming request, the component makes an outbound request whose URL doesn't include the hostname/port. Spin 2.4.3 has been released to fix this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
spin-sdkcrates.io | < 2.4.3 | 2.4.3 |
Patches
3b3db535c9edbed8a665fb92eb3db535c9edbMerge pull request from GHSA-f3h7-gpjj-wcvh
4 files changed · +81 −30
crates/trigger-http/benches/baseline.rs+6 −1 modified@@ -114,7 +114,12 @@ async fn run(trigger: &HttpTrigger, path: &str) { .body(Default::default()) .unwrap(); let resp = trigger - .handle(req, Scheme::HTTP, "127.0.0.1:55555".parse().unwrap()) + .handle( + req, + Scheme::HTTP, + "127.0.0.1:3000".parse().unwrap(), + "127.0.0.1:55555".parse().unwrap(), + ) .await .unwrap(); assert_http_response_success(&resp);
crates/trigger-http/src/lib.rs+47 −28 modified@@ -175,9 +175,9 @@ impl TriggerExecutor for HttpTrigger { let self_ = Arc::new(self); if let Some(tls) = tls { - self_.serve_tls(listener, tls).await? + self_.serve_tls(listener, listen_addr, tls).await? } else { - self_.serve(listener).await? + self_.serve(listener, listen_addr).await? }; Ok(()) @@ -227,9 +227,10 @@ impl HttpTrigger { &self, mut req: Request<Body>, scheme: Scheme, - addr: SocketAddr, + server_addr: SocketAddr, + client_addr: SocketAddr, ) -> Result<Response<Body>> { - set_req_uri(&mut req, scheme)?; + set_req_uri(&mut req, scheme, server_addr)?; strip_forbidden_headers(&mut req); spin_telemetry::extract_trace_context(&req); @@ -273,15 +274,27 @@ impl HttpTrigger { let res = match executor { HttpExecutorType::Http => { HttpHandlerExecutor - .execute(self.engine.clone(), &self.base, &route_match, req, addr) + .execute( + self.engine.clone(), + &self.base, + &route_match, + req, + client_addr, + ) .await } HttpExecutorType::Wagi(wagi_config) => { let executor = WagiHttpExecutor { wagi_config: wagi_config.clone(), }; executor - .execute(self.engine.clone(), &self.base, &route_match, req, addr) + .execute( + self.engine.clone(), + &self.base, + &route_match, + req, + client_addr, + ) .await } }; @@ -347,14 +360,18 @@ impl HttpTrigger { fn serve_connection<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>( self: Arc<Self>, stream: S, - addr: SocketAddr, + server_addr: SocketAddr, + client_addr: SocketAddr, ) { task::spawn(async move { if let Err(e) = http1::Builder::new() .keep_alive(true) .serve_connection( TokioIo::new(stream), - service_fn(move |request| self.clone().instrumented_service_fn(addr, request)), + service_fn(move |request| { + self.clone() + .instrumented_service_fn(server_addr, client_addr, request) + }), ) .await { @@ -365,10 +382,11 @@ impl HttpTrigger { async fn instrumented_service_fn( self: Arc<Self>, - addr: SocketAddr, + server_addr: SocketAddr, + client_addr: SocketAddr, request: Request<Incoming>, ) -> Result<Response<HyperOutgoingBody>> { - let span = http_span!(request, addr); + let span = http_span!(request, client_addr); let method = request.method().to_string(); async { let result = self @@ -378,7 +396,8 @@ impl HttpTrigger { .boxed() }), Scheme::HTTP, - addr, + server_addr, + client_addr, ) .await; finalize_http_span(result, method) @@ -387,22 +406,28 @@ impl HttpTrigger { .await } - async fn serve(self: Arc<Self>, listener: TcpListener) -> Result<()> { + async fn serve(self: Arc<Self>, listener: TcpListener, listen_addr: SocketAddr) -> Result<()> { self.print_startup_msgs("http", &listener)?; loop { - let (stream, addr) = listener.accept().await?; - Self::serve_connection(self.clone(), stream, addr); + let (stream, client_addr) = listener.accept().await?; + self.clone() + .serve_connection(stream, listen_addr, client_addr); } } - async fn serve_tls(self: Arc<Self>, listener: TcpListener, tls: TlsConfig) -> Result<()> { + async fn serve_tls( + self: Arc<Self>, + listener: TcpListener, + listen_addr: SocketAddr, + tls: TlsConfig, + ) -> Result<()> { let acceptor = tls.server_config()?; self.print_startup_msgs("https", &listener)?; loop { let (stream, addr) = listener.accept().await?; match acceptor.accept(stream).await { - Ok(stream) => self.clone().serve_connection(stream, addr), + Ok(stream) => self.clone().serve_connection(stream, listen_addr, addr), Err(err) => tracing::error!(?err, "Failed to start TLS session"), } } @@ -440,21 +465,15 @@ fn parse_listen_addr(addr: &str) -> anyhow::Result<SocketAddr> { addrs.into_iter().next().context("couldn't resolve address") } -fn set_req_uri(req: &mut Request<Body>, scheme: Scheme) -> Result<()> { - const DEFAULT_HOST: &str = "localhost"; - - let authority_hdr = req - .headers() - .get(http::header::HOST) - .map(|h| h.to_str().context("Expected UTF8 header value (authority)")) - .unwrap_or(Ok(DEFAULT_HOST))?; +/// The incoming request's scheme and authority +/// +/// The incoming request's URI is relative to the server, so we need to set the scheme and authority +fn set_req_uri(req: &mut Request<Body>, scheme: Scheme, addr: SocketAddr) -> Result<()> { let uri = req.uri().clone(); let mut parts = uri.into_parts(); - parts.authority = authority_hdr - .parse() - .map(Option::Some) - .map_err(|e| anyhow::anyhow!("Invalid authority {:?}", e))?; + let authority = format!("{}:{}", addr.ip(), addr.port()).parse().unwrap(); parts.scheme = Some(scheme); + parts.authority = Some(authority); *req.uri_mut() = Uri::from_parts(parts).unwrap(); Ok(()) }
tests/integration.rs+26 −0 modified@@ -1395,4 +1395,30 @@ route = "/..." Ok(()) } + + #[test] + /// Test that the HOST header does not allow outbound http arbitrary calls to hosts + fn test_spin_inbound_http_host_header() -> anyhow::Result<()> { + run_test( + "outbound-http-to-same-app", + SpinAppType::Http, + [], + testing_framework::ServicesConfig::none(), + move |env| { + let spin = env.runtime_mut(); + assert_spin_request( + spin, + Request::full( + Method::GET, + "/test/outbound-allowed/hello", + &[("Host", "google.com")], + Some(""), + ), + Response::new_with_body(200, "Hello, Fermyon!\n"), + )?; + Ok(()) + }, + )?; + Ok(()) + } }
tests/testing-framework/src/runtimes/in_process_spin.rs+2 −1 modified@@ -33,7 +33,8 @@ impl InProcessSpin { .handle( req, http::uri::Scheme::HTTP, - (std::net::Ipv4Addr::LOCALHOST, 80).into(), + (std::net::Ipv4Addr::LOCALHOST, 3000).into(), + (std::net::Ipv4Addr::LOCALHOST, 7000).into(), ) .await?; use http_body_util::BodyExt;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.