Add user agent blocklist, use X-Real-IP header for IP detection

This commit is contained in:
2025-11-10 16:06:30 +03:00
Unverified
parent 320442c5fc
commit 0f86e9541c
11 changed files with 499 additions and 0 deletions
+35
View File
@@ -17,6 +17,8 @@ import os
from security.audit import log_security
from security.profanity import contains_profanity
from security.user_agent_blocklist import is_user_agent_blocked
from security.rate_limit import rate_limit_per_ip, rate_limit_per_user
router = APIRouter()
_FAILED_ATTEMPT_WINDOW_SECONDS = 300
@@ -65,9 +67,25 @@ def check_auth(current_user: User = Depends(get_current_user)):
@router.post("/login")
@rate_limit_per_ip("5/minute")
def login(request: LoginRequest, http: Request, db: Session = Depends(get_db)):
username = request.username.strip()
client_ip = get_client_ip(http)
raw_ua = http.headers.get("user-agent")
if is_user_agent_blocked(raw_ua):
log_security(
"blocked_user_agent",
severity="warning",
username=username,
ip=client_ip,
user_agent=raw_ua or "Unknown",
action="login",
)
raise HTTPException(
status_code=403,
detail="Доступ запрещён"
)
user = db.query(User).filter(User.username == username).first()
@@ -162,12 +180,28 @@ def login(request: LoginRequest, http: Request, db: Session = Depends(get_db)):
@router.post("/register")
@rate_limit_per_ip("3/hour")
def register(request: RegisterRequest, http: Request, db: Session = Depends(get_db)):
username = request.username.strip()
display_name = request.display_name.strip()
password = request.password.strip()
confirm_password = request.confirm_password.strip()
client_ip = get_client_ip(http)
raw_ua = http.headers.get("user-agent")
if is_user_agent_blocked(raw_ua):
log_security(
"blocked_user_agent",
severity="warning",
username=username,
ip=client_ip,
user_agent=raw_ua or "Unknown",
action="registration",
)
raise HTTPException(
status_code=403,
detail="Доступ запрещён"
)
# Determine if owner already exists
owner_exists = db.query(User).filter(User.username == OWNER_USERNAME).first() is not None
@@ -411,6 +445,7 @@ def logout(
@router.post("/change-password")
@rate_limit_per_user("5/hour")
def change_password(
request: ChangePasswordRequest,
http: Request,
+6
View File
@@ -27,6 +27,7 @@ import json
from better_profanity import profanity as _bp
from security.audit import log_access, log_dm, log_public_chat, log_security
from security.profanity import censor_text
from security.rate_limit import rate_limit_per_user
router = APIRouter()
logger = logging.getLogger("uvicorn.error")
@@ -251,6 +252,7 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict:
}
@router.post("/send_message")
@rate_limit_per_user("30/minute")
async def send_message(
request: SendMessageRequest | None = None,
current_user: User = Depends(get_current_user),
@@ -408,6 +410,7 @@ async def get_messages(db: Session = Depends(get_db)):
@router.post("/dm/send")
@rate_limit_per_user("20/minute")
async def dm_send(
payload: dict | None = None,
current_user: User = Depends(get_current_user),
@@ -619,6 +622,7 @@ async def get_dm_conversations(current_user: User = Depends(get_current_user), d
@router.put("/edit_message/{message_id}")
@rate_limit_per_user("20/minute")
async def edit_message(
message_id: int,
request: EditMessageRequest,
@@ -694,6 +698,7 @@ async def delete_message(
@router.post("/add_reaction")
@rate_limit_per_user("50/minute")
async def add_reaction(
request: ReactionRequest,
current_user: User = Depends(get_current_user),
@@ -761,6 +766,7 @@ async def add_reaction(
@router.post("/dm/add_reaction")
@rate_limit_per_user("50/minute")
async def add_dm_reaction(
request: DMReactionRequest,
current_user: User = Depends(get_current_user),
+49
View File
@@ -7,6 +7,13 @@ from dependencies import get_current_user
from models import User
from security.audit import log_security
from security.profanity import add_to_blocklist, get_blocklist, remove_from_blocklist
from security.user_agent_blocklist import (
add_to_blocklist as add_ua_to_blocklist,
get_blocklist as get_ua_blocklist,
get_static_blocklist as get_ua_static_blocklist,
get_external_blocklist as get_ua_external_blocklist,
remove_from_blocklist as remove_ua_from_blocklist,
)
class BlocklistUpdateRequest(BaseModel):
@@ -58,3 +65,45 @@ def delete_from_blocklist(
)
return {"removed": removed, "words": updated}
@router.get("/user-agent-blocklist")
def list_user_agent_blocklist(current_user: User = Depends(get_current_user)):
_ensure_owner(current_user)
return {
"patterns": get_ua_blocklist(),
"static": get_ua_static_blocklist(),
"external": get_ua_external_blocklist(),
}
@router.post("/user-agent-blocklist")
def append_user_agent_blocklist(
request: BlocklistUpdateRequest,
current_user: User = Depends(get_current_user)
):
_ensure_owner(current_user)
added, updated = add_ua_to_blocklist(request.words)
log_security(
"user_agent_blocklist_add",
actor=current_user.username,
actor_id=current_user.id,
added=added,
)
return {"added": added, "patterns": updated}
@router.delete("/user-agent-blocklist")
def delete_from_user_agent_blocklist(
request: BlocklistUpdateRequest,
current_user: User = Depends(get_current_user)
):
_ensure_owner(current_user)
removed, updated = remove_ua_from_blocklist(request.words)
log_security(
"user_agent_blocklist_remove",
actor=current_user.username,
actor_id=current_user.id,
removed=removed,
)
return {"removed": removed, "patterns": updated}
+4
View File
@@ -16,6 +16,7 @@ from similarity import is_user_similar_to_verified
from .messaging import messagingManager
from security.audit import log_security
from security.profanity import contains_profanity
from security.rate_limit import rate_limit_per_user
router = APIRouter()
@@ -39,6 +40,7 @@ PROFILE_PICTURES_DIR = Path("data/uploads/pfp")
os.makedirs(PROFILE_PICTURES_DIR, exist_ok=True)
@router.post("/upload-profile-picture")
@rate_limit_per_user("10/minute")
async def upload_profile_picture(
profile_picture: UploadFile = File(...),
current_user: User = Depends(get_current_user),
@@ -163,6 +165,7 @@ async def list_users(
}
@router.put("/user/profile")
@rate_limit_per_user("10/minute")
async def update_user_profile(
request: UpdateProfileRequest,
current_user: User = Depends(get_current_user),
@@ -239,6 +242,7 @@ async def update_user_profile(
@router.put("/user/bio")
@rate_limit_per_user("10/minute")
async def update_user_bio(
request: UpdateBioRequest,
current_user: User = Depends(get_current_user),