Summary
The per-module [capabilities.http].allow outbound HTTP allowlist matches on the request's hostname string only, before any DNS resolution happens. There is no mechanism that resolves the hostname, checks the resolved IP, and pins that IP for the actual connection — the code's own doc comment says as much.
Affected code
crates/nexum-runtime/src/host/http.rs — HttpGate::send_request(), admit()
Finding
admit()'s doc comment: "Allowlist decision for one request URI. Host-only, case-insensitive, exact or *.suffix per host_allowed; IPv6 literals stay bracketed. Name-based and pre-resolution, so there is no IP pinning or DNS-rebinding defence."
Confirmed at the call site: admit() checks uri.host() against the allowlist, and — if it passes — hands the request to default_send_request_handler (wasmtime-wasi-http's hyper+rustls backend), which performs its own DNS resolution at actual connect time, independently and later.
The string-matching logic itself is solid (exact/wildcard-suffix, no normalization tricks, case-insensitive, correctly rejects numeric-IP encodings, userinfo tricks, and backslash confusion — all covered by existing first-party tests) — this isn't a parsing bug, it's an architectural property of hostname-based allowlisting in general.
Impact
If an operator adds some-vendor-api.com to a module's allowlist, and that domain's DNS is later compromised, subject to a subdomain takeover, or hijacked (even briefly, for one connection) to resolve to 169.254.169.254 (cloud metadata) or an internal RFC1918 address, the request still passes the gate and reaches whatever that resolves to at connect time. The allowlist is a hostname-authorization gate, not an IP-destination gate.
Suggested fix
If SSRF-via-compromised-allowlisted-domain is in scope for the threat model, add a second, independent control: resolve the host and reject private/loopback/link-local/metadata-range IPs at connect time, regardless of hostname allowlist status. This is a materially different mechanism from the current host_allowed() string match, not a tweak to it.
Found via
Internal red-team exercise (malicious network attacker persona).
Summary
The per-module
[capabilities.http].allowoutbound HTTP allowlist matches on the request's hostname string only, before any DNS resolution happens. There is no mechanism that resolves the hostname, checks the resolved IP, and pins that IP for the actual connection — the code's own doc comment says as much.Affected code
crates/nexum-runtime/src/host/http.rs—HttpGate::send_request(),admit()Finding
admit()'s doc comment: "Allowlist decision for one request URI. Host-only, case-insensitive, exact or*.suffixperhost_allowed; IPv6 literals stay bracketed. Name-based and pre-resolution, so there is no IP pinning or DNS-rebinding defence."Confirmed at the call site:
admit()checksuri.host()against the allowlist, and — if it passes — hands the request todefault_send_request_handler(wasmtime-wasi-http's hyper+rustls backend), which performs its own DNS resolution at actual connect time, independently and later.The string-matching logic itself is solid (exact/wildcard-suffix, no normalization tricks, case-insensitive, correctly rejects numeric-IP encodings, userinfo tricks, and backslash confusion — all covered by existing first-party tests) — this isn't a parsing bug, it's an architectural property of hostname-based allowlisting in general.
Impact
If an operator adds
some-vendor-api.comto a module's allowlist, and that domain's DNS is later compromised, subject to a subdomain takeover, or hijacked (even briefly, for one connection) to resolve to169.254.169.254(cloud metadata) or an internal RFC1918 address, the request still passes the gate and reaches whatever that resolves to at connect time. The allowlist is a hostname-authorization gate, not an IP-destination gate.Suggested fix
If SSRF-via-compromised-allowlisted-domain is in scope for the threat model, add a second, independent control: resolve the host and reject private/loopback/link-local/metadata-range IPs at connect time, regardless of hostname allowlist status. This is a materially different mechanism from the current
host_allowed()string match, not a tweak to it.Found via
Internal red-team exercise (
malicious network attackerpersona).