Skip to content

HTTP capability allowlist is hostname-based and pre-resolution — no DNS-rebinding/IP-pinning defense #63

Description

@lgahdl

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.rsHttpGate::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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions