Skip to content

Commit 7d50a57

Browse files
committed
feat(cli): port DNS CIDR precheck to p3 HTTP hook
Previously only the p2 send_request path ran the DNS + CIDR deny precheck. wasip3 HTTP clients (including wasi-fetch, which ACT components use internally) bypassed the check and could have been coaxed via SSRF-by-name into a deny-CIDR host. cidr_precheck now returns Result<(), ()> instead of Result<(), P2ErrorCode> so both hooks share the function and map the unit error to their respective p2/p3 HttpRequestDenied variant.
1 parent 53a1f2e commit 7d50a57

1 file changed

Lines changed: 21 additions & 10 deletions

File tree

act-cli/src/runtime/http_policy.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ impl wasmtime_wasi_http::p2::WasiHttpHooks for PolicyHttpHooks {
196196
// connect, worst case is a second resolution that
197197
// mis-steers — we've still blocked the original
198198
// resolved target.
199-
if let Err(code) = cidr_precheck(&cfg, request.uri(), use_tls).await {
200-
return Ok(Err(code));
199+
if cidr_precheck(&cfg, request.uri(), use_tls).await.is_err() {
200+
return Ok(Err(P2ErrorCode::HttpRequestDenied));
201201
}
202202
Ok(wasmtime_wasi_http::p2::default_send_request_handler(request, config).await)
203203
});
@@ -207,11 +207,13 @@ impl wasmtime_wasi_http::p2::WasiHttpHooks for PolicyHttpHooks {
207207
}
208208
}
209209

210-
/// Resolve the request's authority to one or more socket addresses and check
211-
/// each against the configured deny CIDRs. Any hit short-circuits the
212-
/// request with `HttpRequestDenied`.
213-
async fn cidr_precheck(cfg: &HttpConfig, uri: &Uri, use_tls: bool) -> Result<(), P2ErrorCode> {
214-
// No CIDR deny rules → nothing to do.
210+
/// Resolve the request's authority to one or more socket addresses and
211+
/// check each against configured deny CIDRs. Returns `Err(())` on any deny
212+
/// hit; callers map to their p2/p3 `HttpRequestDenied` error variant.
213+
/// `Ok(())` on no CIDR deny rules at all, missing authority, DNS lookup
214+
/// failure (let the downstream handler surface that with its own
215+
/// diagnostics), or no matching rule.
216+
async fn cidr_precheck(cfg: &HttpConfig, uri: &Uri, use_tls: bool) -> Result<(), ()> {
215217
if cfg.deny.iter().all(|r| r.cidr.is_none()) {
216218
return Ok(());
217219
}
@@ -228,14 +230,12 @@ async fn cidr_precheck(cfg: &HttpConfig, uri: &Uri, use_tls: bool) -> Result<(),
228230
for addr in addrs {
229231
if is_ip_denied_by_cidr(cfg, addr.ip(), addr.port()) {
230232
tracing::warn!(%addr, uri = %uri, "http policy: connect blocked by deny CIDR");
231-
return Err(P2ErrorCode::HttpRequestDenied);
233+
return Err(());
232234
}
233235
}
234236
Ok(())
235237
}
236238
Err(e) => {
237-
// DNS failed — let the downstream handler produce a proper DNS
238-
// error with its own diagnostics.
239239
tracing::debug!(%e, uri = %uri, "http policy: DNS precheck lookup failed; deferring to handler");
240240
Ok(())
241241
}
@@ -272,8 +272,19 @@ impl wasmtime_wasi_http::p3::WasiHttpHooks for PolicyHttpHooks {
272272
Decision::Allow => {
273273
tracing::debug!(?method, %uri, "http policy allow (p3)");
274274
let _ = fut;
275+
let cfg = self.config.clone();
276+
// p3 doesn't expose `use_tls` in RequestOptions; infer from
277+
// the URI scheme (the default handler does the same).
278+
let use_tls = uri.scheme_str() == Some("https");
275279
Box::new(async move {
276280
use http_body_util::BodyExt;
281+
// DNS + CIDR deny precheck mirrors the p2 path. Closes
282+
// SSRF-by-name for wasip3 HTTP clients (e.g. wasi-fetch).
283+
if cidr_precheck(&cfg, &uri, use_tls).await.is_err() {
284+
return Err(TrappableError::<P3ErrorCode>::from(
285+
P3ErrorCode::HttpRequestDenied,
286+
));
287+
}
277288
let (res, io) = wasmtime_wasi_http::p3::default_send_request(request, options)
278289
.await
279290
.map_err(TrappableError::<P3ErrorCode>::from)?;

0 commit comments

Comments
 (0)