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: