Sandbox Network Isolation#
Dify's sandbox environment routes all outbound HTTP/S traffic through a Squid-based SSRF proxy. As of Dify 1.15.0, this proxy enforces a deny-by-default policy that blocks all private/RFC-1918 IP ranges and only allows .marketplace.dify.ai out of the box. Workflows or HTTP-request nodes that call internal services will return TCP_DENIED/403 after upgrading from earlier versions.
Network Topology#
Docker Compose defines a dedicated internal bridge network named ssrf_proxy_network with internal: true. This means containers on that network cannot reach the Docker host's external network directly; all egress must traverse the proxy.
┌─ default network (external access) ─────────────┐
│ nginx ←──→ api / worker / plugin_daemon │
└──────────────────────────────────────────────────┘
│ ssrf_proxy_network (internal)
↓
ssrf_proxy (Squid :3128)
│
sandbox (port 8194)
Services that join both networks (api, worker, worker_beat, plugin_daemon) can reach the Squid proxy for outbound calls while still communicating with the rest of the stack on the default network . The sandbox service joins only ssrf_proxy_network and is intentionally cut off from the default network .
The sandbox container is pre-configured to send all traffic through the proxy via HTTP_PROXY and HTTPS_PROXY environment variables :
HTTP_PROXY: http://ssrf_proxy:3128
HTTPS_PROXY: http://ssrf_proxy:3128
Service-name DNS resolution (e.g., ssrf_proxy) works because Docker's embedded DNS resolver is available to every container in a shared network — including the internal: true ssrf_proxy_network.
Squid Deny-by-Default Policy#
The proxy configuration in docker/ssrf_proxy/squid.conf.template enforces the following access control order:
- Include override file first —
/etc/squid/dify_allow_private.confis evaluated before any deny rules , so environment-variable-based exceptions always win. - Allow static allowlist —
acl allowed_domains dstdomain .marketplace.dify.aiis the only hardcoded outbound domain . - Block private IPs — the
to_private_networksACL covers 14 ranges :
0.0.0.0/8,10.0.0.0/8,100.64.0.0/10,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,224.0.0.0/4,240.0.0.0/4, plus IPv6 equivalents (::1/128,fc00::/7,fe80::/10, etc.) - Deny everything else —
http_access deny allas the final rule .
Adding Exceptions#
The docker/ssrf_proxy/docker-entrypoint.sh reads two .env variables at startup and generates Squid ACL fragments into /etc/squid/dify_allow_private.conf :
| Variable | Effect |
|---|---|
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS | Allows named hosts/domains that resolve to private IPs (comma-separated) |
SSRF_PROXY_ALLOW_PRIVATE_IPS | Allows specific private IPs or CIDR ranges (comma-separated) |
These are also exposed as environment pass-throughs in the ssrf_proxy service definition .
Example .env additions:
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=internal.mycompany.com
SSRF_PROXY_ALLOW_PRIVATE_IPS=10.10.5.42,192.168.1.0/24
After editing .env, restart the proxy: docker compose restart ssrf_proxy (or down/up to regenerate configs).
API-Layer SSRF Detection#
api/core/helper/ssrf_proxy.py routes all outbound requests through the Squid proxy via configurable proxy mounts . When Squid blocks a request and returns a 401 or 403, the code checks for squid in the Server or Via response header and raises ToolSSRFError . No local DNS or IP validation is performed — all enforcement is delegated to the proxy.
Key Files#
| File | Purpose |
|---|---|
docker/docker-compose.yaml | Defines ssrf_proxy_network and which services join it |
docker/ssrf_proxy/squid.conf.template | Squid ACL rules and deny-by-default policy |
docker/ssrf_proxy/docker-entrypoint.sh | Generates runtime ACL fragments from env vars |
api/core/helper/ssrf_proxy.py | API-layer proxy routing and ToolSSRFError detection |