From e9d195a2ecfb0dccdbccdef3b2f960869ae88bd2 Mon Sep 17 00:00:00 2001 From: CaffeineFueled Date: Mon, 25 May 2026 01:57:01 +0200 Subject: [PATCH] sec: FIX: rate-limit fix to work with the actual http proxy header --- README.md | 2 ++ app.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7183784..2deca5a 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ podman run -d --name aukpad-app \ -e MAX_TEXT_SIZE=5 \ -e MAX_CONNECTIONS_PER_IP=20 \ -e RETENTION_HOURS=72 \ + -e TRUST_PROXY=true \ git.uphillsecurity.com/cf7/aukpad:latest ``` @@ -118,6 +119,7 @@ The following environment variables can be configured: | `MAX_CONNECTIONS_PER_IP` | `10` | Maximum concurrent connections per IP address | | `RETENTION_HOURS` | `48` | How long to retain pads in hours after last access | | `MAX_ROOMS` | `10000` | Maximum number of pads kept in memory; new pads are refused (WS close 1008) when full until cleanup reclaims space | +| `TRUST_PROXY` | `false` | If `true`, read the client IP from `X-Forwarded-For` (first entry) or `X-Real-IP` for per-IP rate/connection limits. Only enable when aukpad sits behind a reverse proxy that strips/sets these headers — otherwise they can be spoofed | | `DESCRIPTION` | `powered by aukpad.com` | Instance description shown on info page | --- diff --git a/app.py b/app.py index be66cc7..8f456a8 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,7 @@ MAX_TEXT_SIZE = int(os.getenv("MAX_TEXT_SIZE", "5")) * 1024 * 1024 # 5MB defaul MAX_CONNECTIONS_PER_IP = int(os.getenv("MAX_CONNECTIONS_PER_IP", "10")) RETENTION_HOURS = int(os.getenv("RETENTION_HOURS", "48")) # Default 48 hours MAX_ROOMS = int(os.getenv("MAX_ROOMS", "10000")) +TRUST_PROXY = os.getenv("TRUST_PROXY", "false").lower() == "true" DESCRIPTION = os.getenv("DESCRIPTION", "powered by aukpad.com") DOC_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$") @@ -22,6 +23,18 @@ DOC_ID_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$") def is_valid_doc_id(doc_id: str) -> bool: return bool(DOC_ID_RE.match(doc_id)) +def get_client_ip(conn) -> str: + # Only honor proxy headers when explicitly opted in — otherwise attackers + # can spoof them to bypass per-IP limits. + if TRUST_PROXY: + xff = conn.headers.get("x-forwarded-for") + if xff: + return xff.split(",")[0].strip() + xri = conn.headers.get("x-real-ip") + if xri: + return xri.strip() + return conn.client.host if conn.client else "unknown" + # Valkey/Redis client (initialized later if enabled) redis_client = None @@ -601,7 +614,7 @@ def root(): @app.post("/", include_in_schema=False) async def create_pad_with_content(request: Request): # Get client IP - client_ip = request.client.host if request.client else "unknown" + client_ip = get_client_ip(request) # Check rate limit if not check_rate_limit(client_ip): @@ -703,7 +716,7 @@ async def ws(doc_id: str, ws: WebSocket): return # Get client IP for connection limiting - client_ip = ws.client.host if ws.client else "unknown" + client_ip = get_client_ip(ws) # Check connection limit per IP if connections_per_ip[client_ip] >= MAX_CONNECTIONS_PER_IP: