From 075c1694e600225093f64b1d1401406f449fa580 Mon Sep 17 00:00:00 2001 From: Kartik Kenchi Date: Mon, 26 Oct 2026 21:41:26 +0530 Subject: [PATCH] wasi-http: validate the Host header used as an incoming request's authority --- crates/wasi-http/src/authority.rs | 8 +++-- crates/wasi-http/src/handler.rs | 3 +- crates/wasi-http/src/p2/types.rs | 15 ++++++++- crates/wasi-http/src/p2/types_impl.rs | 47 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) diff --git a/crates/wasi-http/src/authority.rs b/crates/wasi-http/src/authority.rs index 5fd1d9f84b88..4c6f5fb04a99 100644 --- a/crates/wasi-http/src/authority.rs +++ b/crates/wasi-http/src/authority.rs @@ -1,6 +1,10 @@ -//! Shared validation for wasi-http outgoing request authorities (p2 and p3). +//! Shared validation for wasi-http request authorities (p2 and p3). -/// Parse an outgoing request `authority`, rejecting a malformed value. +/// Parse a request `authority`, rejecting a malformed value. +/// +/// This is used both for authorities set by guests on outgoing requests and +/// for the `Host` header of incoming requests, which shares the same +/// `uri-host [":" port]` grammar. /// /// `http::uri::Authority` accepts an authority whose port section is empty or /// non-numeric (for example `example.com:` or `example.com:abc`), so the port diff --git a/crates/wasi-http/src/handler.rs b/crates/wasi-http/src/handler.rs index 45e20780d202..139967c1ea57 100644 --- a/crates/wasi-http/src/handler.rs +++ b/crates/wasi-http/src/handler.rs @@ -1005,7 +1005,6 @@ impl<'a, T: Send> Prepared<'a, T> { #[cfg(feature = "p3")] Proxy::P3(guest) => { use http::header::HOST; - use http::uri::Authority; let (parts, body) = request.into_parts(); let http::request::Parts { @@ -1026,7 +1025,7 @@ impl<'a, T: Send> Prepared<'a, T> { None => request_headers .get(HOST) .and_then(|host| host.to_str().ok()) - .and_then(|host| host.parse::().ok()), + .and_then(|host| crate::parse_authority(host.to_string()).ok()), }; let hooks = view(store.data_mut()).hooks; let headers = crate::FieldMap::new_immutable(hooks, request_headers); diff --git a/crates/wasi-http/src/p2/types.rs b/crates/wasi-http/src/p2/types.rs index c6c13c179cab..f16aebb6fca1 100644 --- a/crates/wasi-http/src/p2/types.rs +++ b/crates/wasi-http/src/p2/types.rs @@ -88,7 +88,20 @@ impl WasiHttpCtxView<'_> { let authority = match parts.uri.authority() { Some(authority) => authority.to_string(), None => match parts.headers.get(http::header::HOST) { - Some(host) => host.to_str()?.to_string(), + // RFC 9112 section 3.2 requires a 400 for a `Host` header with + // an invalid field value, so don't pass one on to the guest as + // the request's authority. + Some(host) => match host + .to_str() + .ok() + .and_then(|host| crate::parse_authority(host.to_string()).ok()) + { + Some(authority) => authority.to_string(), + None => { + return Err(wasmtime::Error::msg("invalid HTTP request host header") + .context(ErrorResponse::new(http::StatusCode::BAD_REQUEST))); + } + }, None => { return Err(wasmtime::Error::msg( "invalid HTTP request missing authority in URI and host header", diff --git a/crates/wasi-http/src/p2/types_impl.rs b/crates/wasi-http/src/p2/types_impl.rs index d43616049881..7c84e66ab0f0 100644 --- a/crates/wasi-http/src/p2/types_impl.rs +++ b/crates/wasi-http/src/p2/types_impl.rs @@ -1448,4 +1448,51 @@ mod tests { ); } } + + /// An incoming request's `Host` header becomes its authority, so it must be + /// a valid `uri-host [":" port]` rather than passed through verbatim. + #[test] + fn incoming_request_host_header() { + use crate::p2::bindings::http::types::{HostIncomingRequest as _, Scheme}; + + let mut table = wasmtime::component::ResourceTable::new(); + let mut http = WasiHttpCtx::new(); + let hooks: &mut dyn crate::WasiHttpHooks = crate::default_hooks(); + let mut view = WasiHttpCtxView { + ctx: &mut http, + table: &mut table, + hooks, + }; + let request = |host: &str| { + hyper::Request::builder() + .uri("/") + .header(http::header::HOST, host) + .body(http_body_util::Empty::::new()) + .unwrap() + }; + + for host in ["example.com", "example.com:8080", "[::1]:443"] { + let id = view + .new_incoming_request(Scheme::Http, request(host)) + .unwrap(); + assert_eq!(view.authority(id).unwrap().as_deref(), Some(host)); + } + + for host in [ + "evil.com#.example.com", + "evil.com/x?.example.com", + "user@evil.com", + "example.com:abc", + "a b", + ] { + let r = view.new_incoming_request(Scheme::Http, request(host)); + let err = r.expect_err(host); + assert_eq!( + err.downcast_ref::() + .map(|e| e.status()), + Some(http::StatusCode::BAD_REQUEST), + "host {host:?}" + ); + } + } }