{"record":{"id":"ff3cbca85d763247","repo":"astrid-runtime/astrid","slug":"dns-resolved-to-an-unauthorized-private-or-local-i","errorCode":null,"errorMessage":"DNS resolved to an unauthorized private or local IP address","messagePattern":"DNS resolved to an unauthorized private or local IP address","errorType":"exception","errorClass":"std::io::Error::PermissionDenied","httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule/src/engine/wasm/host/http/ssrf.rs","lineNumber":89,"sourceCode":"                    // not-found as some other kind is a safe degradation\n                    // (falls back to a connection error). Mirrors the `tripped`\n                    // recovery channel.\n                    if lookup_err_is_not_found(&e) {\n                        dns_failed.store(true, Ordering::Relaxed);\n                    }\n                    return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);\n                },\n            };\n\n            let (safe_addrs, saw_unsafe) = filter_safe_addrs(addrs, exempt);\n\n            if safe_addrs.is_empty() {\n                // All resolved addresses failed the airlock: a genuine SSRF\n                // block. Mark `tripped` so the caller can emit the typed\n                // `airlock-rejected` instead of a generic connection error.\n                if saw_unsafe {\n                    tripped.store(true, Ordering::Relaxed);\n                    return Err(Box::new(std::io::Error::new(\n                        std::io::ErrorKind::PermissionDenied,\n                        \"DNS resolved to an unauthorized private or local IP address\",\n                    ))\n                        as Box<dyn std::error::Error + Send + Sync>);\n                }\n                // Resolved to an empty address set: an ordinary resolution miss,\n                // not an airlock block — mark `dns_failed`, not `tripped`.\n                dns_failed.store(true, Ordering::Relaxed);\n                return Err(Box::new(std::io::Error::new(\n                    std::io::ErrorKind::NotFound,\n                    \"host did not resolve to any address\",\n                ))\n                    as Box<dyn std::error::Error + Send + Sync>);\n            }\n\n            let iter: reqwest::dns::Addrs = Box::new(safe_addrs.into_iter());\n            Ok(iter)\n        })","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule/src/engine/wasm/host/http/ssrf.rs#L71-L107","documentation":"The capsule's SSRF-safe DNS resolver (SafeDnsResolver::resolve, used as reqwest's custom resolver) filtered every address the DNS lookup returned through the airlock and none were allowed: at least one resolved address was private/loopback/link-local, so the fetch is rejected with PermissionDenied and the `tripped` flag is set so the host emits a typed `airlock-rejected` error. This prevents sandboxed WASM guests from reaching internal networks, cloud metadata endpoints, or localhost.","triggerScenarios":"A guest makes an HTTP request to a hostname whose DNS records point only to private/loopback/link-local addresses (e.g. 127.0.0.1, 10.x, 169.254.169.254) and that host is not the operator's exempt/allowlisted host; also when DNS rebinding makes a public name resolve to an internal IP.","commonSituations":"Guest code fetching http://localhost:8080 or an internal service name like http://metadata.google.internal; corporate DNS resolving internal-only names for the sandbox; a redirect to an internal host; testing against a locally-running server without registering it as the exempt host.","solutions":["Use the operator's exempt-host/allowlist mechanism (exempt_host at pre-flight) to sanction the specific local endpoint the guest must reach","Have the guest call the public hostname instead of an internal IP or localhost name","If the endpoint must be internal, run the request outside the capsule airlock in trusted host code rather than guest code","Do not weaken filter_safe_addrs — treat this error as a signal that guest code is attempting an SSRF-style request"],"exampleFix":"// before: guest fetches a local dev server\nlet resp = http.get(\"http://localhost:9000/api\").send()?; // airlock-rejected\n// after: register the endpoint as the sanctioned exempt host, then fetch the same name\nSafeDnsResolver::with_exempt_host(\"localhost:9000\");\nlet resp = http.get(\"http://localhost:9000/api\").send()?;","handlingStrategy":"validation","validationCode":"// Before issuing the guest request, check the host will pass the airlock:\nfn resolves_public(host: &str) -> bool {\n    tokio::net::lookup_host((host, 0u16))\n        .map(|addrs| addrs.all(|a| !is_private_or_local(a.ip())))\n        .unwrap_or(false)\n}","typeGuard":"fn is_public_addr(addr: &SocketAddr) -> bool {\n    !(addr.ip().is_loopback()\n        || addr.ip().is_private()\n        || addr.ip().is_link_local()\n        || addr.ip().is_unspecified())\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"unauthorized private or local IP\") => {\n        eprintln!(\"blocked by capsule airlock: host resolves to a private/local address\");\n        // check tripped flag / `airlock-rejected` typed signal, do not retry\n    }\n    other => ignore_or_propagate(other),\n}","preventionTips":["Never point guest code at localhost, 127.0.0.1, 169.254.169.254, or RFC1918 addresses","Register any required local endpoint as the operator's exempt host before the request","Treat this error as a possible SSRF attempt by guest code and audit the source","Watch for DNS-rebinding: validate that public hostnames do not resolve to internal ranges"],"tags":["ssrf","dns","security","network","sandbox"],"backgroundTag":"ssrf-private-ip-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}