Trust X-Forwarded-For only from ArvanCloud edges

nginx on the server also answers on its own IP. A direct request with a fresh
X-Forwarded-For each time got a new throttle identity every time and skipped
every per-IP limit (OTP request/login, admin login): reproduced on the server
2026-10-13, 65 of 65 health requests with rotating XFF got 200.

TrustedProxyMiddleware reads X-Real-IP (set by nginx from the TCP peer) and,
when the peer is not in TRUSTED_PROXY_NETWORKS (default: ArvanCloud's
published list), replaces XFF with the peer address. Logs a WARNING once an
hour per untrusted peer that sent a proxied XFF, so a new Arvan range shows up.

Also ignore the parallel test DB clones (.test-db_N.sqlite3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 18:45:27 +03:30
parent 78c3669119
commit e3bddb3757
4 changed files with 139 additions and 1 deletions

View File

@@ -91,6 +91,52 @@ class APIRequestLoggingMiddleware:
logger.exception('api_request logging failed')
class TrustedProxyMiddleware:
"""
X-Forwarded-For فقط وقتی پذیرفته می‌شود که درخواست واقعاً از ArvanCloud آمده باشد.
NUM_PROXIES=2 فرض می‌کند همیشه آروان و nginx جلوی ما هستند؛ ولی nginxِ این ماشین روی IPِ خودش
(پورت ۸۰ و ۴۴۳) هم جواب می‌دهد. کسی که مستقیم به IP بزند و در هر درخواست یک XFFِ تازه بفرستد،
برای DRF هر بار «کاربرِ تازه» بود و از همه‌ی سقف‌های IP-محور (درخواستِ کد، حدسِ کد، ورودِ ادمین)
رد می‌شد (۲۰۲۶-۱۰-۱۳ روی سرور بازتولید شد: ۶۵ از ۶۵ درخواستِ health با XFFِ متفاوت ۲۰۰).
X-Real-IP را nginx خودش از آدرسِ TCP می‌گذارد (`proxy_params`ِ سایتِ `mathapp`؛ مقدارِ کلاینت را
بازنویسی می‌کند)، پس جعل‌پذیر نیست. اگر آن آدرس در TRUSTED_PROXY_NETWORKS نیست، XFF دور ریخته و
خودِ آن آدرس IPِ کاربر می‌شود. بی X-Real-IP (runserverِ محلی، تست‌ها) دست نمی‌زند.
سنجیده نشد: رنج‌های IPv6ِ آروان (فهرستِ آروان در این تاریخ فقط IPv4 داشت).
"""
WARN_EVERY_SECONDS = 60 * 60
def __init__(self, get_response):
from django.conf import settings
self.get_response = get_response
self.networks = settings.TRUSTED_PROXY_NETWORKS
def _trusted(self, peer: str) -> bool:
import ipaddress
try:
address = ipaddress.ip_address(peer)
except ValueError:
return False
return any(address in network for network in self.networks)
def __call__(self, request):
peer = request.META.get('HTTP_X_REAL_IP', '').strip()
if peer and not self._trusted(peer):
forwarded = request.META.get('HTTP_X_FORWARDED_FOR', '')
# XFF با دو مقدار یعنی پراکسیِ دیگری جلوی nginx بوده: یا کسی XFF جعل کرده، یا آروان رنجِ
# تازه‌ای گرفته که در فهرست نیست. دومی همه‌ی کاربرانِ آن edge را یک IP می‌کند و سقف‌ها برایشان
# زود پر می‌شود؛ پس هر peer ساعتی یک بار WARNING می‌گیرد تا در لاگ دیده شود.
if forwarded.count(',') >= 1:
from django.core.cache import cache
if cache.add(f'untrusted_proxy_warned:{peer}', 1, self.WARN_EVERY_SECONDS):
logger.warning('X-Forwarded-For from a peer outside TRUSTED_PROXY_NETWORKS ignored: '
'peer=%s (a new ArvanCloud range would also look like this)', peer)
request.META['HTTP_X_FORWARDED_FOR'] = peer
return self.get_response(request)
class APINoStoreMiddleware:
"""
هر پاسخِ `/api/` هدرِ `Cache-Control: no-store` می‌گیرد، مگر خودِ view