zeroclaw-labs/zeroclaw · error
Blocked marker redirect to private or local host ({host}); r
Error message
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. What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/matrix.rs:3277
}
// `attempt.url()` borrows the attempt, so we copy out the
// bits we need into owned Strings before `attempt.error(...)`,
// which moves the attempt, can run.
let target_str = attempt.url().as_str().to_string();
let host = attempt.url().host_str().unwrap_or("").to_string();
if zeroclaw_tools::helpers::domain_guard::is_private_or_local_host(&host) {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"target": target_str,
"host": host,
"reason": "ssrf_redirect_to_private_host",
})),
"matrix: marker redirect targets a private/local host"
);
return attempt.error(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"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."
),
));
}
attempt.follow()
});
reqwest::Client::builder()
.timeout(MARKER_HTTP_TIMEOUT)
.redirect(redirect_policy)
.user_agent("zeroclaw-matrix/1.0")
.build()
.expect("default reqwest client config never fails to build")
})
}View on GitHub (pinned to 88bb9c8533)
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.
Example fix
# before: marker redirects to an internal media host
{"matrix:file":"https://media.internal.example/_matrix/media/..."} # 301 -> http://10.0.0.5/...
# after: serve publicly or attach locally
cp report.pdf /var/lib/zeroclaw/workspace/agent/report.pdf
{"path":"report.pdf"} # resolved from workspace_dir, no HTTP fetch Defensive patterns
Strategy: validation
Validate before calling
fn is_public_host(url: &str) -> anyhow::Result<bool> {
let host = url::Url::parse(url)?
.host_str()
.ok_or_else(|| anyhow!("url has no host"))?;
// resolve and reject loopback / private / link-local before fetching
for ip in std::net::ToSocketAddrs::to_socket_addrs(&format!("{host}:0"))? {
if ip.ip().is_loopback() || ip.ip().is_private() || ip.ip().is_link_local() || ip.ip().is_unspecified() {
return Ok(false);
}
}
Ok(true)
} Try / catch
match matrix.fetch_marker(url).await {
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied && e.to_string().contains("SSRF") => {
// policy: never bypass; fall back to local workspace attachment
attach_from_workspace(&marker.path).await
}
other => other,
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Generated image URL targets a local or non-global host
- Blocked local/private host: {display_host}
- Cross-host redirects are blocked so DNS validation remains p
- PermissionDenied
- Blocked local/private host: {host}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/91baf673f0c3f456.
Report an issue: GitHub.