feat(auth): SA3 — trusted-proxy X-Forwarded-For for rate limiting
_client_ip trusted the first X-Forwarded-For hop unconditionally, so anyone able to reach the app port could forge XFF and dodge the login/register/reset/demo rate limits. Now trust XFF ONLY when the request's socket peer is a configured reverse proxy (settings.trusted_proxy_ips, e.g. the VPS Caddy's WireGuard peer IP), and take the RIGHTMOST entry — the client our proxy actually saw and appended, immune to a client pre-seeding a fake XFF. A request from any other peer (hitting the port directly) is keyed on its real socket IP, so XFF can't be forged to bypass the limits. New TRUSTED_PROXY_IPS env (empty default = no proxy, use the direct peer). Documented in .env.example, docs/self-hosting.md, README. Unit-verified against spoof-through-proxy and direct-bypass cases.
This commit is contained in:
parent
2eca56d68a
commit
7297dbd2a8
5 changed files with 52 additions and 7 deletions
|
|
@ -173,12 +173,19 @@ def get_or_create_demo_user(db: Session) -> User:
|
|||
|
||||
|
||||
def _client_ip(request: Request) -> str:
|
||||
"""Best-effort client IP for rate limiting. Behind our reverse proxy (Caddy/NPM) the
|
||||
real client is the first X-Forwarded-For hop; fall back to the socket peer otherwise."""
|
||||
xff = request.headers.get("x-forwarded-for")
|
||||
if xff:
|
||||
return xff.split(",")[0].strip()
|
||||
return request.client.host if request.client else "unknown"
|
||||
"""Best-effort client IP for rate limiting. X-Forwarded-For is client-controlled, so we trust it
|
||||
ONLY when the request actually arrived from a configured reverse proxy (settings.trusted_proxy_ips
|
||||
— the address the proxy connects FROM, e.g. the VPS Caddy's WireGuard peer IP). Our proxy APPENDS
|
||||
the client it saw to XFF, so the RIGHTMOST entry is the real client and can't be forged by a
|
||||
client pre-seeding the header. Any other peer (someone hitting the app port directly, bypassing the
|
||||
proxy) is untrusted — we use its real socket IP, so it can't spoof its rate-limit identity."""
|
||||
peer = request.client.host if request.client else "unknown"
|
||||
if peer in settings.trusted_proxy_ip_set:
|
||||
xff = request.headers.get("x-forwarded-for")
|
||||
parts = [p.strip() for p in (xff or "").split(",") if p.strip()]
|
||||
if parts:
|
||||
return parts[-1]
|
||||
return peer
|
||||
|
||||
|
||||
def upsert_pending_invite(db: Session, email: str) -> Invite | None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue