{"record":{"id":"edc8a77e2b600487","repo":"zeroclaw-labs/zeroclaw","slug":"generated-image-url-targets-a-cloud-metadata-host","errorCode":null,"errorMessage":"Generated image URL targets a cloud metadata host","messagePattern":"Generated image URL targets a cloud metadata host","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/zeroclaw-tools/src/image_gen.rs","lineNumber":51,"sourceCode":"    }\n    if !url.username().is_empty() || url.password().is_some() {\n        anyhow::bail!(\"Generated image URL userinfo is not allowed\");\n    }\n\n    let request_host = url\n        .host_str()\n        .ok_or_else(|| anyhow::Error::msg(\"Generated image URL must include a host\"))?;\n    if request_host.ends_with('.') {\n        anyhow::bail!(\"Generated image URL host must not end with a dot\");\n    }\n    let host = domain_guard::normalize_domain(request_host)\n        .ok_or_else(|| anyhow::Error::msg(\"Generated image URL host is invalid\"))?;\n    let ip_literal = host.parse::<IpAddr>().ok();\n    if domain_guard::is_private_or_local_host(&host) {\n        anyhow::bail!(\"Generated image URL targets a local or non-global host\");\n    }\n    if ip_literal.is_some_and(domain_guard::is_cloud_metadata_ip) {\n        anyhow::bail!(\"Generated image URL targets a cloud metadata host\");\n    }\n\n    // Request the host that was validated, not the raw spelling. Normalization\n    // can change the host (a leading dot is stripped), and the DNS pin applied\n    // by `generated_image_client_with_builder` is keyed on the normalized host.\n    // Without this, `resolve_to_addrs` never matches the request host and\n    // reqwest silently falls back to its own unvalidated lookup. IP-literal\n    // hosts are left alone: they carry no DNS pin, and `set_host` rejects the\n    // unbracketed IPv6 form that normalization produces.\n    if ip_literal.is_none() {\n        url.set_host(Some(&host))\n            .map_err(|_| anyhow::Error::msg(\"Generated image URL host is invalid\"))?;\n    }\n\n    let port = url\n        .port_or_known_default()\n        .ok_or_else(|| anyhow::Error::msg(\"Generated image URL must include a valid port\"))?;\n    Ok((url, host, port))","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/image_gen.rs#L33-L69","documentation":"When the generated image URL's host is an IP literal, parse_public_https_url checks it against domain_guard::is_cloud_metadata_ip, which covers the entire 169.254.0.0/16 IPv4 link-local range (AWS/GCP/ECS task metadata at 169.254.169.254, ECS 169.254.170.2/23), Alibaba's metadata address, Azure's WireServer platform address, AWS fd00:ec2::/64, the GCP IPv6 metadata address, and IPv4-mapped/NAT64-embedded forms of those addresses. Cloud metadata endpoints hand out instance credentials, so they are refused unconditionally — even the private-resolution opt-in that exists for http_request never re-opens them.","triggerScenarios":"The image URL host is a metadata IP literal such as https://169.254.169.254/... , https://100.100.100.200/..., https://168.63.129.16/..., or an IPv6/NAT64 form embedding one of those; typically the result of prompt injection trying to exfiltrate instance credentials, or a misconstructed test URL.","commonSituations":"Adversarial prompts embedding metadata URLs to test SSRF hardening, security scans of agent deployments, and accidentally pasted cloud-debug links into image fields.","solutions":["Never target metadata addresses from the image pipeline; if a legitimate service uses 169.254.x.x, give it a routable address or public name.","Treat occurrences of this error in logs as a security signal: inspect the prompt/response that produced the URL for injection attempts.","Check URL-building code that mangles addresses into link-local form."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"fn is_metadata_literal(host: &str) -> bool {\n    if let Ok(ip) = host.trim_end_matches('.').parse::<std::net::IpAddr>() {\n        let v4 = match ip { std::net::IpAddr::V4(v) => Some(v), std::net::IpAddr::V6(_) => None };\n        return v4.is_some_and(|v| v.octets()[0] == 169 && v.octets()[1] == 254);\n    }\n    false\n}","typeGuard":"fn is_safe_image_url_host(u: &reqwest::Url) -> bool {\n    u.host_str().is_some_and(|h| !is_metadata_literal(h))\n}","tryCatchPattern":"if let Err(e) = validate_image_target(url, nat64).await {\n    if e.to_string().contains(\"cloud metadata host\") {\n        // security event: log the originating prompt/response and fail closed; do not retry or rewrite the URL\n    }\n}","preventionTips":["Treat any metadata-literal image URL as a prompt-injection indicator; alert on it rather than routing around it.","Never place instance credentials (metadata service) in the reach of URL-driven fetch paths.","There is no opt-in: both public-only and private-allowed modes refuse metadata addresses."],"tags":["ssrf","security","cloud-metadata","image-gen","hardening","rust"],"backgroundTag":"ssrf-metadata-endpoint-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}