SSRF Proxy#
Dify routes all outbound HTTP requests through a Squid-based SSRF proxy layer to prevent Server-Side Request Forgery. The system has two components:
- Squid sidecar containers β enforce network-level ACL rules (deny-by-default since v1.15.0)
- API-layer client β
api/core/helper/ssrf_proxy.pyβ handles transport, retries, SSL verification, and error translation
Two independent Squid services run in Docker :
| Service | Used by | Policy |
|---|---|---|
ssrf_proxy | API, worker, agent_backend | Deny-by-default; configurable exceptions |
agent_ssrf_proxy | local_sandbox (Agent V2) | Deny-by-default; hardcoded paths + configurable exceptions |
The API container connects to Squid via SSRF_PROXY_HTTP_URL / SSRF_PROXY_HTTPS_URL (or SSRF_PROXY_ALL_URL) . When no proxy URL is set, the client falls back to a direct connection with no SSRF protection.
Note:
ssrf_proxy.pyis for outbound API calls only β remote file downloads belong incore.file.remote_fetcher.
Retry Logic & Exponential Backoff#
make_request() runs its own retry loop:
- Retried status codes (
STATUS_FORCELIST):429, 500, 502, 503, 504 - Backoff formula:
sleep(0.5 Γ 2^(retry - 1))β 0.5 s, 1 s, 2 s, 4 s, β¦ - Default max retries: 3, controlled by
SSRF_DEFAULT_MAX_RETRIES - When
max_retries=0,httpx.RequestErroris re-raised immediately - Exhausting all retries raises
MaxRetriesExceededError
β οΈ Retry Multiplication (fixed in PR #33689)#
When an HTTP Request Node has graph-level retry configured in the workflow UI, two independent retry mechanisms previously fired on the same failure. With graph retries = 5 and SSRF retries = 3, a single failure triggered 24 requests instead of 6: (3 + 1) Γ (5 + 1) = 24 .
Fix: The HTTP Request node now passes max_retries=0 to make_request() when graph-level retry is active, delegating all retry control to the graph handler . When no graph retry is configured, SSRF retries operate at the default of 3.
Timeout Configuration#
All four httpx timeout dimensions are externalized as environment variables β defaults are 5 seconds each :
| Env var | Default | Meaning |
|---|---|---|
SSRF_DEFAULT_TIME_OUT | 5 s | Overall request timeout |
SSRF_DEFAULT_CONNECT_TIME_OUT | 5 s | TCP connect timeout |
SSRF_DEFAULT_READ_TIME_OUT | 5 s | Socket read timeout |
SSRF_DEFAULT_WRITE_TIME_OUT | 5 s | Socket write timeout |
Callers can override any timeout by passing an explicit timeout kwarg to make_request() .
Squid itself enforces separate timeouts in squid-common.conf.template (connect_timeout 30s, request_timeout 2min, read_timeout 2min) β these are currently hardcoded, not configurable via environment variables .
Note: The pre-1.15.0 env var
HTTP_REQUEST_MAX_READ_TIMEOUTno longer controls network timeouts; that is now managed by the SSRF proxy layer .
Squid ACL Policy#
Since v1.15.0, both proxies enforce a deny-by-default policy . The to_private_networks ACL blocks 14 ranges: RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback, link-local, CGN, and IPv6 equivalents . The only hardcoded allowlist entry is .marketplace.dify.ai .
SSRF detection: When Squid blocks a request with 401 or 403, make_request() checks for squid in the Server / Via response header and raises ToolSSRFError .
Common symptoms after upgrade to 1.15.0+ :
- HTTP Request nodes return
403 Forbiddensurfaced asToolSSRFError - Worker logs show
TCP_DENIED/403inssrf_proxycontainer output MaxRetriesExceededErrorwhen backends are slow (Squid 502 β retry loop exhausted)
Operator Allowlisting#
Both ssrf_proxy and agent_ssrf_proxy support runtime exceptions via .env :
| Env var | Effect |
|---|---|
SSRF_PROXY_ALLOW_PRIVATE_IPS | Specific private IPs or CIDR ranges |
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS | Named hosts/domains resolving to private IPs |
At startup, docker-entrypoint.sh and docker-agent-entrypoint.sh each generate /etc/squid/dify_allow_private.conf from these variables; this file is evaluated before the private-network deny rule .
# .env additions
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=internal.mycompany.com,ollama
SSRF_PROXY_ALLOW_PRIVATE_IPS=10.10.5.42,192.168.1.0/24
# Apply
docker compose restart ssrf_proxy agent_ssrf_proxy
Squid Image Version Pinning#
The ssrf_proxy and agent_ssrf_proxy services use ubuntu/squid. Newer tags (e.g., ubuntu/squid:latest resolving to 7.2-26.04_beta) changed log directory ownership and pinger behavior, causing containers to crash-loop with Permission denied on /var/log/squid/cache.log and FATAL: Unable to open any ICMP sockets .
Workaround: Pin to a supported, stable tag such as ubuntu/squid:6.6-24.04_beta (supported until 2029) . The entrypoint scripts do not chown /var/log/squid/ before launching Squid, so image upgrades that change the default user can silently break both proxy containers.
Key Files#
| File | Purpose |
|---|---|
api/core/helper/ssrf_proxy.py | SSRF client: make_request(), retry loop, ToolSSRFError detection, proxy mounts |
api/configs/feature/__init__.py | All SSRF_* env var defaults in HttpConfig |
docker/ssrf_proxy/squid.conf.template | Squid ACL rules for ssrf_proxy |
docker/ssrf_proxy/squid-agent.conf.template | ACL rules for agent_ssrf_proxy |
docker/ssrf_proxy/squid-common.conf.template | Shared ACL definitions (to_private_networks, port rules, timeouts) |
docker/ssrf_proxy/docker-entrypoint.sh | Generates dify_allow_private.conf at startup for ssrf_proxy |
docker/ssrf_proxy/docker-agent-entrypoint.sh | Generates dify_allow_private.conf at startup for agent_ssrf_proxy |
api/dify_graph/nodes/http_request/node.py | HTTP Request Node β passes max_retries=0 when graph-level retry is active |
Related: Sandbox Network Isolation Β· Agent Sandbox SSRF Allowlisting Β· HTTP Request Node Β· Release Breaking Changes