{"record":{"id":"91baf673f0c3f456","repo":"zeroclaw-labs/zeroclaw","slug":"blocked-marker-redirect-to-private-or-local-host","errorCode":null,"errorMessage":"Blocked marker redirect to private or local host ({host}); refusing for SSRF safety. Use a public URL or attach the file from workspace_dir directly.","messagePattern":"Blocked marker redirect to private or local host \\((.+?)\\); refusing for SSRF safety\\. Use a public URL or attach the file from workspace_dir directly\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/matrix.rs","lineNumber":3277,"sourceCode":"                }\n                // `attempt.url()` borrows the attempt, so we copy out the\n                // bits we need into owned Strings before `attempt.error(...)`,\n                // which moves the attempt, can run.\n                let target_str = attempt.url().as_str().to_string();\n                let host = attempt.url().host_str().unwrap_or(\"\").to_string();\n                if zeroclaw_tools::helpers::domain_guard::is_private_or_local_host(&host) {\n                    ::zeroclaw_log::record!(\n                        WARN,\n                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                            .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                            .with_attrs(::serde_json::json!({\n                                \"target\": target_str,\n                                \"host\": host,\n                                \"reason\": \"ssrf_redirect_to_private_host\",\n                            })),\n                        \"matrix: marker redirect targets a private/local host\"\n                    );\n                    return attempt.error(std::io::Error::new(\n                        std::io::ErrorKind::PermissionDenied,\n                        format!(\n                            \"Blocked marker redirect to private or local host ({host}); \\\n                             refusing for SSRF safety. Use a public URL or attach the file \\\n                             from workspace_dir directly.\"\n                        ),\n                    ));\n                }\n                attempt.follow()\n            });\n            reqwest::Client::builder()\n                .timeout(MARKER_HTTP_TIMEOUT)\n                .redirect(redirect_policy)\n                .user_agent(\"zeroclaw-matrix/1.0\")\n                .build()\n                .expect(\"default reqwest client config never fails to build\")\n        })\n    }","sourceCodeStart":3259,"sourceCodeEnd":3295,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/matrix.rs#L3259-L3295","documentation":"While the Matrix channel resolves a file marker, an HTTP redirect whose resolved host is private/loopback/link-local is refused with PermissionDenied. This is an SSRF guard: marker URLs are attacker-influenced room content, and following them to internal hosts (127.0.0.1, 10.x, 192.168.x, ::1, 169.254.x) would let room content probe the host's internal network. The error text tells you the two supported alternatives.","triggerScenarios":"A Matrix message carries a file/URL marker; the target answers with a 3xx redirect chain that lands on a private or local IP, for example a self-hosted homeserver behind a reverse proxy that redirects to its internal upstream, or a malicious room deliberately pointing at internal metadata services.","commonSituations":"Self-hosted Synapse behind nginx/treffelp proxy redirecting to 127.0.0.1, matrix-media-repo deployed on an internal address, DNS that resolves a public-looking name to an RFC1918 address inside the network, untrusted room content in open channels.","solutions":["Do not try to bypass the guard; publish the file at a genuinely public URL and let the marker point there.","Attach the file from workspace_dir directly, as the error message suggests: place the artifact in the agent workspace and reference it locally instead of via HTTP.","Fix the redirect source: reconfigure the homeserver or reverse proxy so the canonical public URL serves content directly instead of bouncing to an internal host.","If an internal fetch is legitimately required, fetch it out-of-band into workspace_dir with your own audited tooling, then reference the local copy."],"exampleFix":"# before: marker redirects to an internal media host\n{\"matrix:file\":\"https://media.internal.example/_matrix/media/...\"} # 301 -> http://10.0.0.5/...\n\n# after: serve publicly or attach locally\ncp report.pdf /var/lib/zeroclaw/workspace/agent/report.pdf\n{\"path\":\"report.pdf\"} # resolved from workspace_dir, no HTTP fetch","handlingStrategy":"validation","validationCode":"fn is_public_host(url: &str) -> anyhow::Result<bool> {\n    let host = url::Url::parse(url)?\n        .host_str()\n        .ok_or_else(|| anyhow!(\"url has no host\"))?;\n    // resolve and reject loopback / private / link-local before fetching\n    for ip in std::net::ToSocketAddrs::to_socket_addrs(&format!(\"{host}:0\"))? {\n        if ip.ip().is_loopback() || ip.ip().is_private() || ip.ip().is_link_local() || ip.ip().is_unspecified() {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}","typeGuard":null,"tryCatchPattern":"match matrix.fetch_marker(url).await {\n    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains(\"SSRF\") => {\n        // policy: never bypass; fall back to local workspace attachment\n        attach_from_workspace(&marker.path).await\n    }\n    other => other,\n}","preventionTips":["Publish files at public URLs instead of relying on homeserver-internal addresses for markers.","Prefer workspace_dir attachments for anything the agent must read; they bypass HTTP entirely.","Audit your reverse-proxy config so public matrix media URLs do not redirect to internal upstreams."],"tags":["matrix","ssrf","redirect","security","network"],"backgroundTag":"ssrf-redirect-blocked","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}