Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,20 @@ RUN set -eu; \
# OpenClaw's web_fetch SSRF guard blocks *.internal hostnames before the
# OpenShell L7 proxy sees the request. NemoClaw users legitimately reach
# host-local approved services through host.openshell.internal after the
# OpenShell policy explicitly allows that host:port. Allow only this exact
# hostname, only inside an OpenShell sandbox, and only at the hostname-only
# check used by trusted env-proxy mode. Direct DNS-pinned/private-IP paths
# remain blocked, and metadata/link-local/private IP literals are unchanged.
# OpenShell policy explicitly allows that host:port. Add this exact hostname
# only to the web_fetch trusted-env-proxy policy, only inside an OpenShell
# sandbox. The generic SSRF helper and strict/direct DNS-pinned paths remain
# unmodified, so metadata/link-local/private IP literals are unchanged.
#
# === Removal criteria ===
# Patch 1: drop when OpenClaw deprecates withStrictGuardedFetchMode or
# when all media-fetch callsites unconditionally pass useEnvProxy.
# Patch 2: drop when OpenClaw fixes assertExplicitProxyAllowed to skip the
# target hostname allowlist for the proxy hostname check (or exposes config
# to disable the check).
# Patch 2b: drop when OpenClaw ships a reviewed host-gateway SSRF policy
# surface that can allow host.openshell.internal without allowing broader
# private/special-use hostnames.
# Patch 2b: drop when OpenClaw ships a reviewed web_fetch trusted-proxy SSRF
# policy surface that can allow host.openshell.internal without allowing
# broader private/special-use hostnames.
#
# SYNC WITH OPENCLAW: these patches classify the compiled OpenClaw dist at
# build time. They apply the legacy patch when the old target exists, skip
Expand Down Expand Up @@ -278,31 +278,37 @@ RUN set -eu; \
patch_fail "Patch 2 cannot safely skip"; \
fi; \
fi; \
# --- Patch 2b: allow OpenShell host gateway hostname in trusted proxy mode --- \
ssrf_hostname_files="$(grep -RIlE --include='*.js' 'function assertHostnameAllowedWithPolicy\(hostname, policy\)' "$OC_DIST" || true)"; \
if [ -n "$ssrf_hostname_files" ]; then \
# --- Patch 2b: allow OpenShell host gateway only through web_fetch trusted env proxy --- \
# Reviewed against openclaw@2026.5.22 dist: fetchWithWebToolsNetworkGuard \
# passes useEnvProxy into withTrustedEnvProxyGuardedFetchMode(resolved), and \
# the SSRF guard consumes policy.allowedHostnames to skip private-network \
# checks for an exact normalized hostname. hostnameAllowlist only gates \
# hostname pattern matching and does not bypass .internal/private blocking. \
web_guard_files="$(grep -RIlE --include='*.js' 'function fetchWithWebToolsNetworkGuard\(params\)' "$OC_DIST" || true)"; \
if [ -n "$web_guard_files" ]; then \
patched_host_gateway=0; \
for f in $ssrf_hostname_files; do \
if grep -q 'nemoclaw: OpenShell host gateway' "$f"; then \
for f in $web_guard_files; do \
if grep -q 'nemoclaw: OpenShell host gateway for web_fetch trusted env proxy' "$f"; then \
echo "INFO: Patch 2b already present in $f"; \
else \
grep -q 'normalizeHostname' "$f" || patch_fail "Patch 2b target $f is missing normalizeHostname"; \
sed -i -E 's|(function assertHostnameAllowedWithPolicy\(hostname, policy\) \{)|\1 const normalizedHost = normalizeHostname(hostname); if (process.env.OPENSHELL_SANDBOX === "1" \&\& normalizedHost === "host.openshell.internal") return normalizedHost; /* nemoclaw: OpenShell host gateway via trusted proxy, see Dockerfile */ |' "$f"; \
grep -Eq 'assertHostnameAllowedWithPolicy\(hostname, policy\) \{ const normalizedHost = normalizeHostname\(hostname\); if \(process\.env\.OPENSHELL_SANDBOX === "1" && normalizedHost === "host\.openshell\.internal"\) return normalizedHost; /\* nemoclaw: OpenShell host gateway' "$f" \
grep -q 'withTrustedEnvProxyGuardedFetchMode(resolved)' "$f" \
|| patch_fail "Patch 2b target $f is missing reviewed trusted env-proxy web_fetch call"; \
sed -i -E 's|return fetchWithSsrFGuard\(useEnvProxy \? withTrustedEnvProxyGuardedFetchMode\(resolved\) : withStrictGuardedFetchMode\(resolved\)\);|const hostGatewayPolicy = process.env.OPENSHELL_SANDBOX === "1" \&\& useEnvProxy \&\& new URL(resolved.url).hostname === "host.openshell.internal" ? { ...resolved.policy, allowedHostnames: [...resolved.policy?.allowedHostnames ?? [], "host.openshell.internal"] } : resolved.policy; return fetchWithSsrFGuard(useEnvProxy ? withTrustedEnvProxyGuardedFetchMode({ ...resolved, policy: hostGatewayPolicy }) : withStrictGuardedFetchMode(resolved)); /* nemoclaw: OpenShell host gateway for web_fetch trusted env proxy, see Dockerfile */|' "$f"; \
grep -Fq 'process.env.OPENSHELL_SANDBOX === "1" && useEnvProxy && new URL(resolved.url).hostname === "host.openshell.internal"' "$f" \
|| patch_fail "Patch 2b verification failed for $f"; \
patched_host_gateway=1; \
fi; \
done; \
if [ "$patched_host_gateway" = "1" ]; then \
echo "INFO: Patch 2b applied to OpenClaw ${OC_VERSION} host-gateway hostname validator"; \
echo "INFO: Patch 2b applied to OpenClaw ${OC_VERSION} web_fetch trusted-proxy host-gateway policy"; \
fi; \
else \
internal_hostname_blocks="$(grep -RIlE --include='*.js' '\.internal|Blocked hostname or private/internal/special-use IP address|assertHostnameAllowedWithPolicy' "$OC_DIST" || true)"; \
if [ -z "$internal_hostname_blocks" ]; then \
echo "INFO: OpenClaw ${OC_VERSION} has no host-gateway hostname validator; Patch 2b not needed"; \
web_fetch_proxy_refs="$(grep -RIlE --include='*.js' 'web_fetch|useEnvProxy|useTrustedEnvProxy|withTrustedEnvProxyGuardedFetchMode\(resolved\)' "$OC_DIST" || true)"; \
if [ -z "$web_fetch_proxy_refs" ]; then \
echo "INFO: OpenClaw ${OC_VERSION} has no web_fetch trusted env-proxy callsite; Patch 2b not needed"; \
else \
echo "ERROR: Patch 2b target missing but internal-hostname SSRF blocks remain:" >&2; \
printf '%s\n' "$internal_hostname_blocks" | head -n 5 >&2; \
echo "ERROR: Patch 2b target missing but web_fetch/trusted-proxy references remain:" >&2; \
printf '%s\n' "$web_fetch_proxy_refs" | head -n 5 >&2; \
patch_fail "Patch 2b cannot safely skip"; \
fi; \
fi; \
Expand Down
5 changes: 5 additions & 0 deletions scripts/generate-openclaw-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,11 @@ def _placeholder(channel: str, env_key: str) -> str:
"loopbackMode": "proxy",
}

# Keep keyless web_fetch available by default, but force it through the
# trusted env proxy. OpenShell's L7 policy remains the egress authority:
# without an approved host:port, the proxy denies the request. Remove this
# default only if OpenClaw gains a first-class least-privilege web_fetch
# policy that can preserve host-gateway fetch without bypassing OpenShell.
tools_web = config.setdefault("tools", {}).setdefault("web", {})
tools_web["fetch"] = {"enabled": True, "useTrustedEnvProxy": True}

Expand Down
Loading
Loading