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

  1. Do not try to bypass the guard; publish the file at a genuinely public URL and let the marker point there.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/91baf673f0c3f456. Report an issue: GitHub.