security: add brute-force rate limiting to /login
5 failed attempts per IP within 5 minutes triggers 15-minute block. Counter resets on successful login. State is per-worker (in-memory).
This commit is contained in:
@@ -42,6 +42,9 @@ from utils import (
|
||||
from auth import (
|
||||
get_current_user, has_access, issue_auth_cookie, issue_csrf_cookie,
|
||||
require_admin, require_user, user_is_valid, validate_csrf, verify_password, hash_password,
|
||||
check_login_rate_limit,
|
||||
record_login_failure,
|
||||
record_login_success,
|
||||
)
|
||||
from runtime import (
|
||||
acquire_universal_slot, acquire_web_pool_slot, allocator_lock,
|
||||
@@ -1160,8 +1163,13 @@ def login(
|
||||
if not cookie_csrf or csrf_token != cookie_csrf:
|
||||
raise HTTPException(status_code=403, detail="CSRF failed")
|
||||
|
||||
ip = _get_real_ip(request)
|
||||
if check_login_rate_limit(ip):
|
||||
raise HTTPException(status_code=429, detail="Слишком много попыток входа. Попробуйте через 15 минут.")
|
||||
|
||||
user = db.scalar(select(User).where(User.username == username))
|
||||
if not user or not verify_password(password, user.password_hash):
|
||||
record_login_failure(ip)
|
||||
csrf = request.cookies.get(CSRF_COOKIE) or secrets.token_urlsafe(24)
|
||||
response = templates.TemplateResponse(
|
||||
"login.html",
|
||||
@@ -1189,6 +1197,7 @@ def login(
|
||||
response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="lax", path="/")
|
||||
return response
|
||||
|
||||
record_login_success(ip)
|
||||
response = RedirectResponse(url="/", status_code=303)
|
||||
issue_auth_cookie(response, user)
|
||||
issue_csrf_cookie(response)
|
||||
|
||||
Reference in New Issue
Block a user