From 0fd2016cca14da6f8344498a6bf21543984f7879 Mon Sep 17 00:00:00 2001 From: JBHook <314778749+JBHook@users.noreply.github.com> Date: Sun, 9 Aug 2026 04:54:18 +0000 Subject: [PATCH] Fix Lack of Rate Limiting on login /users/v1/login had no limit at all on how many attempts a client could make, making credential stuffing, password spraying, and brute force against any account trivial - nothing in the codebase throttled repeated requests to this endpoint. Added a simple in-memory sliding-window rate limiter keyed by client IP: at most 5 login attempts per 60-second window, after which further attempts get a 429 "Too many login attempts" response until the window rolls forward. This is a minimal, dependency-free implementation (no new package added) appropriate for a single-process app; a production deployment behind a shared cache/reverse proxy would want a centralized limiter instead, but this closes the endpoint's complete lack of any throttling. Verified live: 5 rapid login attempts (correct or incorrect credentials) from the same client succeed/fail normally; the 6th and 7th attempts both get 429 instead of being processed, including a request with the CORRECT password (rate limiting applies before credentials are even checked); after the 60-second window elapses, a legitimate login succeeds again normally. Co-Authored-By: Claude Sonnet 5 --- api_views/users.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/api_views/users.py b/api_views/users.py index 172540a..b554610 100644 --- a/api_views/users.py +++ b/api_views/users.py @@ -1,7 +1,10 @@ import re +import time import jsonschema import jwt +from collections import defaultdict, deque + from config import db, vuln_app from api_views.json_schemas import * from flask import jsonify, Response, request, json @@ -16,6 +19,26 @@ def error_message_helper(msg): return '{ "status": "fail", "message": "' + msg + '"}' +# Lack of Rate Limiting: /users/v1/login had no limit on how many attempts a client could +# make, making credential stuffing / password spraying / brute force trivial. Tracks login +# attempts per client IP in a sliding window and rejects further attempts once the limit is +# hit within the window. +LOGIN_RATE_LIMIT_MAX_ATTEMPTS = 5 +LOGIN_RATE_LIMIT_WINDOW_SECONDS = 60 +_login_attempts_by_ip = defaultdict(deque) + + +def _is_login_rate_limited(client_ip): + now = time.time() + attempts = _login_attempts_by_ip[client_ip] + while attempts and now - attempts[0] > LOGIN_RATE_LIMIT_WINDOW_SECONDS: + attempts.popleft() + if len(attempts) >= LOGIN_RATE_LIMIT_MAX_ATTEMPTS: + return True + attempts.append(now) + return False + + def get_all_users(): return_value = jsonify({'users': User.get_all_users()}) return return_value @@ -83,6 +106,11 @@ def register_user(): def login_user(): + if _is_login_rate_limited(request.remote_addr): + return Response( + error_message_helper("Too many login attempts. Please try again later."), 429, + mimetype="application/json") + request_data = request.get_json() try: