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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user