Skip to content

Commit 292843d

Browse files
rylevkate-goldenring
authored andcommitted
Stop using untrusted user input for determining outbound request permissions.
[Spin outgoing http requests were using untrusted user input](https://github.com/fermyon/spin/blob/4987d4912a8c6ed766416f2d9544448d3f3bdfe4/crates/trigger-http/src/handler.rs#L367-L369) (namely the value of `Host` header) to determine the origin at which the Spin application is presumably running. Instead of taking the "Host" header as the authority for outbound requests, the host that the server is listening on is used instead.
1 parent 4f63686 commit 292843d

File tree

4 files changed

+81
-30
lines changed

4 files changed

+81
-30
lines changed

crates/trigger-http/benches/baseline.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ async fn run(trigger: &HttpTrigger, path: &str) {
114114
.body(Default::default())
115115
.unwrap();
116116
let resp = trigger
117-
.handle(req, Scheme::HTTP, "127.0.0.1:55555".parse().unwrap())
117+
.handle(
118+
req,
119+
Scheme::HTTP,
120+
"127.0.0.1:3000".parse().unwrap(),
121+
"127.0.0.1:55555".parse().unwrap(),
122+
)
118123
.await
119124
.unwrap();
120125
assert_http_response_success(&resp);

crates/trigger-http/src/lib.rs

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ impl TriggerExecutor for HttpTrigger {
175175

176176
let self_ = Arc::new(self);
177177
if let Some(tls) = tls {
178-
self_.serve_tls(listener, tls).await?
178+
self_.serve_tls(listener, listen_addr, tls).await?
179179
} else {
180-
self_.serve(listener).await?
180+
self_.serve(listener, listen_addr).await?
181181
};
182182

183183
Ok(())
@@ -227,9 +227,10 @@ impl HttpTrigger {
227227
&self,
228228
mut req: Request<Body>,
229229
scheme: Scheme,
230-
addr: SocketAddr,
230+
server_addr: SocketAddr,
231+
client_addr: SocketAddr,
231232
) -> Result<Response<Body>> {
232-
set_req_uri(&mut req, scheme)?;
233+
set_req_uri(&mut req, scheme, server_addr)?;
233234
strip_forbidden_headers(&mut req);
234235

235236
spin_telemetry::extract_trace_context(&req);
@@ -273,15 +274,27 @@ impl HttpTrigger {
273274
let res = match executor {
274275
HttpExecutorType::Http => {
275276
HttpHandlerExecutor
276-
.execute(self.engine.clone(), &self.base, &route_match, req, addr)
277+
.execute(
278+
self.engine.clone(),
279+
&self.base,
280+
&route_match,
281+
req,
282+
client_addr,
283+
)
277284
.await
278285
}
279286
HttpExecutorType::Wagi(wagi_config) => {
280287
let executor = WagiHttpExecutor {
281288
wagi_config: wagi_config.clone(),
282289
};
283290
executor
284-
.execute(self.engine.clone(), &self.base, &route_match, req, addr)
291+
.execute(
292+
self.engine.clone(),
293+
&self.base,
294+
&route_match,
295+
req,
296+
client_addr,
297+
)
285298
.await
286299
}
287300
};
@@ -347,14 +360,18 @@ impl HttpTrigger {
347360
fn serve_connection<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(
348361
self: Arc<Self>,
349362
stream: S,
350-
addr: SocketAddr,
363+
server_addr: SocketAddr,
364+
client_addr: SocketAddr,
351365
) {
352366
task::spawn(async move {
353367
if let Err(e) = http1::Builder::new()
354368
.keep_alive(true)
355369
.serve_connection(
356370
TokioIo::new(stream),
357-
service_fn(move |request| self.clone().instrumented_service_fn(addr, request)),
371+
service_fn(move |request| {
372+
self.clone()
373+
.instrumented_service_fn(server_addr, client_addr, request)
374+
}),
358375
)
359376
.await
360377
{
@@ -365,10 +382,11 @@ impl HttpTrigger {
365382

366383
async fn instrumented_service_fn(
367384
self: Arc<Self>,
368-
addr: SocketAddr,
385+
server_addr: SocketAddr,
386+
client_addr: SocketAddr,
369387
request: Request<Incoming>,
370388
) -> Result<Response<HyperOutgoingBody>> {
371-
let span = http_span!(request, addr);
389+
let span = http_span!(request, client_addr);
372390
let method = request.method().to_string();
373391
async {
374392
let result = self
@@ -378,7 +396,8 @@ impl HttpTrigger {
378396
.boxed()
379397
}),
380398
Scheme::HTTP,
381-
addr,
399+
server_addr,
400+
client_addr,
382401
)
383402
.await;
384403
finalize_http_span(result, method)
@@ -387,22 +406,28 @@ impl HttpTrigger {
387406
.await
388407
}
389408

390-
async fn serve(self: Arc<Self>, listener: TcpListener) -> Result<()> {
409+
async fn serve(self: Arc<Self>, listener: TcpListener, listen_addr: SocketAddr) -> Result<()> {
391410
self.print_startup_msgs("http", &listener)?;
392411
loop {
393-
let (stream, addr) = listener.accept().await?;
394-
Self::serve_connection(self.clone(), stream, addr);
412+
let (stream, client_addr) = listener.accept().await?;
413+
self.clone()
414+
.serve_connection(stream, listen_addr, client_addr);
395415
}
396416
}
397417

398-
async fn serve_tls(self: Arc<Self>, listener: TcpListener, tls: TlsConfig) -> Result<()> {
418+
async fn serve_tls(
419+
self: Arc<Self>,
420+
listener: TcpListener,
421+
listen_addr: SocketAddr,
422+
tls: TlsConfig,
423+
) -> Result<()> {
399424
let acceptor = tls.server_config()?;
400425
self.print_startup_msgs("https", &listener)?;
401426

402427
loop {
403428
let (stream, addr) = listener.accept().await?;
404429
match acceptor.accept(stream).await {
405-
Ok(stream) => self.clone().serve_connection(stream, addr),
430+
Ok(stream) => self.clone().serve_connection(stream, listen_addr, addr),
406431
Err(err) => tracing::error!(?err, "Failed to start TLS session"),
407432
}
408433
}
@@ -440,21 +465,15 @@ fn parse_listen_addr(addr: &str) -> anyhow::Result<SocketAddr> {
440465
addrs.into_iter().next().context("couldn't resolve address")
441466
}
442467

443-
fn set_req_uri(req: &mut Request<Body>, scheme: Scheme) -> Result<()> {
444-
const DEFAULT_HOST: &str = "localhost";
445-
446-
let authority_hdr = req
447-
.headers()
448-
.get(http::header::HOST)
449-
.map(|h| h.to_str().context("Expected UTF8 header value (authority)"))
450-
.unwrap_or(Ok(DEFAULT_HOST))?;
468+
/// The incoming request's scheme and authority
469+
///
470+
/// The incoming request's URI is relative to the server, so we need to set the scheme and authority
471+
fn set_req_uri(req: &mut Request<Body>, scheme: Scheme, addr: SocketAddr) -> Result<()> {
451472
let uri = req.uri().clone();
452473
let mut parts = uri.into_parts();
453-
parts.authority = authority_hdr
454-
.parse()
455-
.map(Option::Some)
456-
.map_err(|e| anyhow::anyhow!("Invalid authority {:?}", e))?;
474+
let authority = format!("{}:{}", addr.ip(), addr.port()).parse().unwrap();
457475
parts.scheme = Some(scheme);
476+
parts.authority = Some(authority);
458477
*req.uri_mut() = Uri::from_parts(parts).unwrap();
459478
Ok(())
460479
}

tests/integration.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,4 +1395,30 @@ route = "/..."
13951395

13961396
Ok(())
13971397
}
1398+
1399+
#[test]
1400+
/// Test that the HOST header does not allow outbound http arbitrary calls to hosts
1401+
fn test_spin_inbound_http_host_header() -> anyhow::Result<()> {
1402+
run_test(
1403+
"outbound-http-to-same-app",
1404+
SpinAppType::Http,
1405+
[],
1406+
testing_framework::ServicesConfig::none(),
1407+
move |env| {
1408+
let spin = env.runtime_mut();
1409+
assert_spin_request(
1410+
spin,
1411+
Request::full(
1412+
Method::GET,
1413+
"/test/outbound-allowed/hello",
1414+
&[("Host", "google.com")],
1415+
Some(""),
1416+
),
1417+
Response::new_with_body(200, "Hello, Fermyon!\n"),
1418+
)?;
1419+
Ok(())
1420+
},
1421+
)?;
1422+
Ok(())
1423+
}
13981424
}

tests/testing-framework/src/runtimes/in_process_spin.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ impl InProcessSpin {
3333
.handle(
3434
req,
3535
http::uri::Scheme::HTTP,
36-
(std::net::Ipv4Addr::LOCALHOST, 80).into(),
36+
(std::net::Ipv4Addr::LOCALHOST, 3000).into(),
37+
(std::net::Ipv4Addr::LOCALHOST, 7000).into(),
3738
)
3839
.await?;
3940
use http_body_util::BodyExt;

0 commit comments

Comments
 (0)