mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
fix(ssrf): apply private allowlist to agent_ssrf_proxy (#41870)
This commit is contained in:
parent
5c0d3c4393
commit
c7423ffb2d
@ -706,6 +706,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
||||||
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
||||||
|
SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}
|
||||||
|
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}
|
||||||
networks:
|
networks:
|
||||||
# Needs to reach api and agent_backend as forward-proxy destinations.
|
# Needs to reach api and agent_backend as forward-proxy destinations.
|
||||||
- default
|
- default
|
||||||
|
|||||||
@ -712,6 +712,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
|
||||||
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
|
||||||
|
SSRF_PROXY_ALLOW_PRIVATE_IPS: ${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}
|
||||||
|
SSRF_PROXY_ALLOW_PRIVATE_DOMAINS: ${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-}
|
||||||
networks:
|
networks:
|
||||||
# Needs to reach api and agent_backend as forward-proxy destinations.
|
# Needs to reach api and agent_backend as forward-proxy destinations.
|
||||||
- default
|
- default
|
||||||
|
|||||||
34
docker/ssrf_proxy/docker-agent-entrypoint.sh
Normal file → Executable file
34
docker/ssrf_proxy/docker-agent-entrypoint.sh
Normal file → Executable file
@ -5,6 +5,40 @@ tail -F /var/log/squid/error.log 2>/dev/null &
|
|||||||
tail -F /var/log/squid/store.log 2>/dev/null &
|
tail -F /var/log/squid/store.log 2>/dev/null &
|
||||||
tail -F /var/log/squid/cache.log 2>/dev/null &
|
tail -F /var/log/squid/cache.log 2>/dev/null &
|
||||||
|
|
||||||
|
ALLOW_PRIVATE_CONF=/etc/squid/dify_allow_private.conf
|
||||||
|
|
||||||
|
write_optional_private_allowlist() {
|
||||||
|
local env_name="$1"
|
||||||
|
local acl_name="$2"
|
||||||
|
local acl_type="$3"
|
||||||
|
local raw_values="${!env_name:-}"
|
||||||
|
local -a tokens=()
|
||||||
|
|
||||||
|
raw_values="${raw_values//,/ }"
|
||||||
|
raw_values="${raw_values//$'\n'/ }"
|
||||||
|
raw_values="${raw_values//$'\r'/ }"
|
||||||
|
|
||||||
|
if [ -z "${raw_values//[[:space:]]/}" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -a tokens <<< "$raw_values"
|
||||||
|
|
||||||
|
printf 'acl %s %s' "$acl_name" "$acl_type" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
for value in "${tokens[@]}"; do
|
||||||
|
[ -z "${value//[[:space:]]/}" ] && continue
|
||||||
|
printf ' %s' "$value" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
done
|
||||||
|
printf '\nhttp_access allow client_localnet %s\n' "$acl_name" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Generated by docker-agent-entrypoint.sh."
|
||||||
|
echo "# Allows selected private targets before the default private-network deny rule."
|
||||||
|
} > "$ALLOW_PRIVATE_CONF"
|
||||||
|
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_IPS" "dify_allowed_private_networks" "dst"
|
||||||
|
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS" "dify_allowed_private_domains" "dstdomain"
|
||||||
|
|
||||||
expand_env() {
|
expand_env() {
|
||||||
awk '{
|
awk '{
|
||||||
while(match($0, /\${[A-Za-z_][A-Za-z_0-9]*}/)) {
|
while(match($0, /\${[A-Za-z_][A-Za-z_0-9]*}/)) {
|
||||||
|
|||||||
@ -17,5 +17,6 @@ http_access deny !Safe_ports
|
|||||||
http_access deny CONNECT !SSL_ports
|
http_access deny CONNECT !SSL_ports
|
||||||
http_access allow dst_agent_backend path_agent_stub
|
http_access allow dst_agent_backend path_agent_stub
|
||||||
http_access allow dst_dify_api path_files
|
http_access allow dst_dify_api path_files
|
||||||
|
include /etc/squid/dify_allow_private.conf
|
||||||
http_access deny to_private_networks
|
http_access deny to_private_networks
|
||||||
http_access allow all
|
http_access allow all
|
||||||
|
|||||||
97
docker/ssrf_proxy/test_agent_ssrf_allowlist_unit.sh
Executable file
97
docker/ssrf_proxy/test_agent_ssrf_allowlist_unit.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
ALLOW_PRIVATE_CONF="$WORK_DIR/dify_allow_private.conf"
|
||||||
|
export ALLOW_PRIVATE_CONF
|
||||||
|
|
||||||
|
write_optional_private_allowlist() {
|
||||||
|
local env_name="$1"
|
||||||
|
local acl_name="$2"
|
||||||
|
local acl_type="$3"
|
||||||
|
local raw_values="${!env_name:-}"
|
||||||
|
local -a tokens=()
|
||||||
|
|
||||||
|
raw_values="${raw_values//,/ }"
|
||||||
|
raw_values="${raw_values//$'\n'/ }"
|
||||||
|
raw_values="${raw_values//$'\r'/ }"
|
||||||
|
|
||||||
|
if [ -z "${raw_values//[[:space:]]/}" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
read -r -a tokens <<< "$raw_values"
|
||||||
|
|
||||||
|
printf 'acl %s %s' "$acl_name" "$acl_type" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
for value in "${tokens[@]}"; do
|
||||||
|
[ -z "${value//[[:space:]]/}" ] && continue
|
||||||
|
printf ' %s' "$value" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
done
|
||||||
|
printf '\nhttp_access allow client_localnet %s\n' "$acl_name" >> "$ALLOW_PRIVATE_CONF"
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_allowlist_config() {
|
||||||
|
{
|
||||||
|
echo "# Generated by docker-agent-entrypoint.sh."
|
||||||
|
echo "# Allows selected private targets before the default private-network deny rule."
|
||||||
|
} > "$ALLOW_PRIVATE_CONF"
|
||||||
|
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_IPS" "dify_allowed_private_networks" "dst"
|
||||||
|
write_optional_private_allowlist "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS" "dify_allowed_private_domains" "dstdomain"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_contains() {
|
||||||
|
local file="$1"
|
||||||
|
local expected="$2"
|
||||||
|
if ! grep -Fq "$expected" "$file"; then
|
||||||
|
echo "Expected generated config to contain: $expected"
|
||||||
|
cat "$file" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_not_contains() {
|
||||||
|
local file="$1"
|
||||||
|
local unexpected="$2"
|
||||||
|
if grep -Fq "$unexpected" "$file"; then
|
||||||
|
echo "Expected generated config NOT to contain: $unexpected"
|
||||||
|
cat "$file" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Empty env vars should produce only the header comments.
|
||||||
|
unset SSRF_PROXY_ALLOW_PRIVATE_IPS SSRF_PROXY_ALLOW_PRIVATE_DOMAINS
|
||||||
|
generate_allowlist_config
|
||||||
|
assert_not_contains "$ALLOW_PRIVATE_CONF" "dify_allowed_private_networks"
|
||||||
|
assert_not_contains "$ALLOW_PRIVATE_CONF" "dify_allowed_private_domains"
|
||||||
|
|
||||||
|
# IP and domain allowlists should match the workflow ssrf_proxy entrypoint format.
|
||||||
|
export SSRF_PROXY_ALLOW_PRIVATE_IPS="172.21.0.0/16,10.0.0.1"
|
||||||
|
export SSRF_PROXY_ALLOW_PRIVATE_DOMAINS="internal_api,.corp.local"
|
||||||
|
generate_allowlist_config
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "acl dify_allowed_private_networks dst 172.21.0.0/16 10.0.0.1"
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "http_access allow client_localnet dify_allowed_private_networks"
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "acl dify_allowed_private_domains dstdomain internal_api .corp.local"
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "http_access allow client_localnet dify_allowed_private_domains"
|
||||||
|
|
||||||
|
# Glob-like tokens and newline separators must not be expanded or split by the shell.
|
||||||
|
export SSRF_PROXY_ALLOW_PRIVATE_IPS=$'172.21.0.0/16\n10.0.0.1'
|
||||||
|
export SSRF_PROXY_ALLOW_PRIVATE_DOMAINS='*.corp.local,?internal.example'
|
||||||
|
generate_allowlist_config
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "acl dify_allowed_private_networks dst 172.21.0.0/16 10.0.0.1"
|
||||||
|
assert_contains "$ALLOW_PRIVATE_CONF" "acl dify_allowed_private_domains dstdomain *.corp.local ?internal.example"
|
||||||
|
|
||||||
|
if ! grep -Fq "include /etc/squid/dify_allow_private.conf" "$ROOT_DIR/squid-agent.conf.template"; then
|
||||||
|
echo "squid-agent.conf.template must include dify_allow_private.conf before the private-network deny rule."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -A1 "include /etc/squid/dify_allow_private.conf" "$ROOT_DIR/squid-agent.conf.template" | grep -Fq "http_access deny to_private_networks"; then
|
||||||
|
echo "dify_allow_private.conf must be included before http_access deny to_private_networks."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Agent SSRF allowlist unit tests passed."
|
||||||
@ -9,6 +9,7 @@ SANDBOX_CONTAINER_NAME="${CONTAINER_NAME}-sandbox"
|
|||||||
AGENT_PROXY_CONTAINER_NAME="${CONTAINER_NAME}-agent-proxy"
|
AGENT_PROXY_CONTAINER_NAME="${CONTAINER_NAME}-agent-proxy"
|
||||||
API_CONTAINER_NAME="${CONTAINER_NAME}-api"
|
API_CONTAINER_NAME="${CONTAINER_NAME}-api"
|
||||||
AGENT_BACKEND_CONTAINER_NAME="${CONTAINER_NAME}-agent-backend"
|
AGENT_BACKEND_CONTAINER_NAME="${CONTAINER_NAME}-agent-backend"
|
||||||
|
INTERNAL_SERVICE_CONTAINER_NAME="${CONTAINER_NAME}-internal-service"
|
||||||
NETWORK_NAME="${SSRF_PROXY_TEST_NETWORK:-dify-ssrf-proxy-test-$$}"
|
NETWORK_NAME="${SSRF_PROXY_TEST_NETWORK:-dify-ssrf-proxy-test-$$}"
|
||||||
RUN_PUBLIC_CHECK="${SSRF_PROXY_TEST_PUBLIC_CHECK:-true}"
|
RUN_PUBLIC_CHECK="${SSRF_PROXY_TEST_PUBLIC_CHECK:-true}"
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ cleanup() {
|
|||||||
docker rm -f "$AGENT_PROXY_CONTAINER_NAME" >/dev/null 2>&1 || true
|
docker rm -f "$AGENT_PROXY_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
docker rm -f "$API_CONTAINER_NAME" >/dev/null 2>&1 || true
|
docker rm -f "$API_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
docker rm -f "$AGENT_BACKEND_CONTAINER_NAME" >/dev/null 2>&1 || true
|
docker rm -f "$AGENT_BACKEND_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
|
docker rm -f "$INTERNAL_SERVICE_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true
|
docker network rm "$NETWORK_NAME" >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +206,16 @@ docker run \
|
|||||||
sh -c "mkdir -p /www/agent-stub && echo stub-ok > /www/agent-stub/config && echo denied > /www/index.html && httpd -f -p 5050 -h /www" \
|
sh -c "mkdir -p /www/agent-stub && echo stub-ok > /www/agent-stub/config && echo denied > /www/index.html && httpd -f -p 5050 -h /www" \
|
||||||
>/dev/null
|
>/dev/null
|
||||||
|
|
||||||
|
# Mock internal service reachable only on the private Docker network.
|
||||||
|
docker run \
|
||||||
|
--detach \
|
||||||
|
--name "$INTERNAL_SERVICE_CONTAINER_NAME" \
|
||||||
|
--network "$NETWORK_NAME" \
|
||||||
|
--network-alias internal_api \
|
||||||
|
"$CLIENT_IMAGE" \
|
||||||
|
sh -c "mkdir -p /www && echo internal-ok > /www/health && httpd -f -p 8080 -h /www" \
|
||||||
|
>/dev/null
|
||||||
|
|
||||||
docker run \
|
docker run \
|
||||||
--detach \
|
--detach \
|
||||||
--name "$AGENT_PROXY_CONTAINER_NAME" \
|
--name "$AGENT_PROXY_CONTAINER_NAME" \
|
||||||
@ -214,6 +226,8 @@ docker run \
|
|||||||
--volume "$ROOT_DIR/docker/ssrf_proxy/docker-agent-entrypoint.sh:/docker-entrypoint-mount.sh:ro" \
|
--volume "$ROOT_DIR/docker/ssrf_proxy/docker-agent-entrypoint.sh:/docker-entrypoint-mount.sh:ro" \
|
||||||
--env HTTP_PORT=3128 \
|
--env HTTP_PORT=3128 \
|
||||||
--env COREDUMP_DIR=/var/spool/squid \
|
--env COREDUMP_DIR=/var/spool/squid \
|
||||||
|
--env "SSRF_PROXY_ALLOW_PRIVATE_IPS=${SSRF_PROXY_ALLOW_PRIVATE_IPS:-}" \
|
||||||
|
--env "SSRF_PROXY_ALLOW_PRIVATE_DOMAINS=${SSRF_PROXY_ALLOW_PRIVATE_DOMAINS:-internal_api}" \
|
||||||
"$IMAGE" \
|
"$IMAGE" \
|
||||||
-c "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" \
|
-c "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" \
|
||||||
>/dev/null
|
>/dev/null
|
||||||
@ -237,6 +251,9 @@ fi
|
|||||||
assert_private_target_blocked "$agent_proxy_url" "http://127.0.0.1:80/"
|
assert_private_target_blocked "$agent_proxy_url" "http://127.0.0.1:80/"
|
||||||
assert_private_target_blocked "$agent_proxy_url" "http://169.254.169.254/latest/meta-data/"
|
assert_private_target_blocked "$agent_proxy_url" "http://169.254.169.254/latest/meta-data/"
|
||||||
|
|
||||||
|
# Allowlisted private domains must be reachable for agent skills.
|
||||||
|
assert_public_target_allowed "$agent_proxy_url" "http://internal_api:8080/health"
|
||||||
|
|
||||||
# agent_backend /agent-stub/* must be allowed.
|
# agent_backend /agent-stub/* must be allowed.
|
||||||
assert_public_target_allowed "$agent_proxy_url" "http://agent_backend:5050/agent-stub/config"
|
assert_public_target_allowed "$agent_proxy_url" "http://agent_backend:5050/agent-stub/config"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user