{"record":{"id":"86a50f55bd92d8d6","repo":"clockworklabs/SpacetimeDB","slug":"refusing-to-connect-to-private-or-special-purpose","errorCode":null,"errorMessage":"refusing to connect to private or special-purpose addresses","messagePattern":"refusing to connect to private or special-purpose addresses","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/core/src/host/instance_env.rs","lineNumber":1036,"sourceCode":"///\n/// If the user requests a timeout longer than this, we will clamp to this value.\n/// 180 seconds accommodates long-running LLM and AI API calls,\n/// which routinely take 30-120 seconds for complex requests.\nconst HTTP_MAX_TIMEOUT: Duration = Duration::from_secs(180);\nconst BLOCKED_HTTP_ADDRESS_ERROR: &str = \"refusing to connect to private or special-purpose addresses\";\n\nstruct FilteredDnsResolver;\n\nimpl reqwest::dns::Resolve for FilteredDnsResolver {\n    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {\n        let host = name.as_str().to_owned();\n        Box::pin(async move {\n            let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;\n            let filtered_addrs: Vec<SocketAddr> = addrs.filter(|addr| !is_blocked_ip(addr.ip())).collect();\n\n            if filtered_addrs.is_empty() {\n                return Err(\n                    std::io::Error::new(std::io::ErrorKind::PermissionDenied, BLOCKED_HTTP_ADDRESS_ERROR).into(),\n                );\n            }\n\n            Ok(Box::new(filtered_addrs.into_iter()) as reqwest::dns::Addrs)\n        })\n    }\n}\n\nfn is_blocked_ip_literal(url: &reqwest::Url) -> bool {\n    match url.host() {\n        Some(url::Host::Ipv4(ip)) => is_blocked_ip(IpAddr::V4(ip)),\n        Some(url::Host::Ipv6(ip)) => is_blocked_ip(IpAddr::V6(ip)),\n        Some(url::Host::Domain(_)) | None => false,\n    }\n}\n\nfn is_blocked_ip(ip: IpAddr) -> bool {\n    match ip {","sourceCodeStart":1018,"sourceCodeEnd":1054,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/core/src/host/instance_env.rs#L1018-L1054","documentation":"SpacetimeDB's host applies SSRF protection to every outgoing HTTP(S) request a database module makes. URLs with an IP-literal host are checked directly, and hostnames resolve through FilteredDnsResolver, which drops every blocked address - RFC 6890 special-purpose ranges including 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16, documentation/benchmarking ranges, and their IPv6 analogues. If no allowed address remains, the request fails with PermissionDenied and this message.","triggerScenarios":"A module HTTP client requesting http://localhost:PORT, http://127.0.0.1, http://169.254.169.254/ (cloud metadata), any 10.x / 172.16-31.x / 192.168.x address, or a DNS name that resolves only to private or loopback IPs (e.g. an internal cluster service name).","commonSituations":"A module calling its own SpacetimeDB node or a sibling internal service during development; webhooks pointing at internal hostnames; localhost dev URLs accidentally shipped to a hosted deployment.","solutions":["Point the module at a public, externally reachable address instead.","If internal egress is genuinely required, route through a public proxy/gateway you control that performs its own authentication.","For local test builds only, the host can be compiled with the allow_loopback_http_for_tests feature to unblock loopback - never enable this in production."],"exampleFix":"// before: dev URL shipped into a hosted module\nlet url = \"http://localhost:8080/api\";\n\n// after: public endpoint reachable from the host's egress\nlet url = \"https://api.example.com/v1\";","handlingStrategy":"validation","validationCode":"use std::net::IpAddr;\n\nfn is_private_or_special(ip: IpAddr) -> bool {\n    match ip {\n        IpAddr::V4(v) => v.is_loopback() || v.is_private() || v.is_link_local() || v.is_unspecified(),\n        IpAddr::V6(v) => v.is_loopback() || v.is_unspecified() || (v.segments()[0] & 0xfe00) == 0xfc00,\n    }\n}\n\n// Resolve the target host first and fail fast with your own message if every\n// address is blocked, instead of discovering it inside the module call.","typeGuard":null,"tryCatchPattern":"match module_http_request(url).await {\n    Err(e) if e.to_string().contains(\"private or special-purpose addresses\") => {\n        // Configuration error, not transient: surface it to the operator and point\n        // the module at a public endpoint or an authenticated proxy.\n    }\n    r => r,\n}","preventionTips":["Never point module HTTP calls at localhost, 127.0.0.1, link-local, or RFC1918 addresses.","Remember DNS names resolving only to private IPs are also blocked.","Keep dev-time localhost URLs out of deployed module configuration."],"tags":["network","ssrf","security","http","spacetimedb"],"backgroundTag":"ssrf-protection-blocked-request","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}