mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 23:57:58 +09:00
Rate-limit buckets, login lockout and audit-log source IPs hand-parsed the leftmost X-Forwarded-For token — a client-controlled value — letting an attacker rotate it to dodge per-IP limits or forge audit trails. - config: add TRUST_PROXY_HEADERS / TRUSTED_PROXY_HOPS (+ _env_bool/_env_int) - create_app: wrap in gated Werkzeug ProxyFix (rightmost trusted hops) before the limiter/handlers so every remote_addr consumer sees the real client - new app/utils/client_ip.py::get_client_ip() — the single trusted seam - replace all six leftmost-XFF parsers (rate_limit, audit middleware, api_key_auth, api_analytics, audit_service, ddns) with get_client_ip() - deploy: root .env.example ships TRUST_PROXY_HEADERS=true (behind nginx); backend/.env.example documents it off for dev - tests: test_trusted_client_ip.py (7) incl. real flask-limiter bucket isolation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31 lines
1.4 KiB
Python
31 lines
1.4 KiB
Python
"""Single seam for deriving the trusted client IP.
|
|
|
|
Every consumer that keys on the client's IP — rate-limit buckets, audit-log
|
|
source IPs, API-key attribution, the per-IP auth throttle, dynamic DNS — must
|
|
call :func:`get_client_ip` instead of hand-parsing ``X-Forwarded-For``. That
|
|
header is client-controlled: its leftmost token is whatever the caller chose,
|
|
so ``X-Forwarded-For.split(',')[0]`` trusts a forgeable value and lets an
|
|
attacker rotate the IP we rate-limit / lock out / audit on.
|
|
|
|
The trust decision lives in exactly one place: the config-gated Werkzeug
|
|
``ProxyFix`` applied in :func:`app.create_app` (see ``config.TRUST_PROXY_HEADERS``
|
|
/ ``config.TRUSTED_PROXY_HOPS``). When a proxy is trusted, ProxyFix has already
|
|
rewritten ``request.remote_addr`` to the real client (the rightmost trusted
|
|
hops of XFF); when it isn't, ``remote_addr`` is the direct socket peer. Either
|
|
way this returns the one value the app has decided to trust — never a
|
|
hand-parsed header.
|
|
"""
|
|
from flask import has_request_context, request
|
|
|
|
|
|
def get_client_ip():
|
|
"""Return the client IP the app trusts for the current request.
|
|
|
|
Post-ProxyFix (when ``TRUST_PROXY_HEADERS`` is on) ``request.remote_addr``
|
|
is the real client IP; otherwise it is the direct socket peer. Returns
|
|
``None`` when called outside a request context.
|
|
"""
|
|
if not has_request_context():
|
|
return None
|
|
return request.remote_addr
|