update: 修复基于长亭monkeycode扫描结果的12处安全漏洞
This commit is contained in:
+25
-8
@@ -6,7 +6,7 @@ from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from sqlalchemy import select
|
||||
@@ -21,6 +21,7 @@ from app.database import get_engine
|
||||
from app.models.user import User as UserModel
|
||||
from app.models.settings import Settings as SettingsModel
|
||||
from app.services.email_service import email_service
|
||||
from app.security import create_session_token
|
||||
|
||||
# 中国时区 UTC+8
|
||||
CHINA_TZ = timezone(timedelta(hours=8))
|
||||
@@ -43,6 +44,7 @@ _state_storage = {}
|
||||
|
||||
# 邮箱验证码临时存储(生产环境应使用 Redis)
|
||||
_email_verification_storage = {}
|
||||
MAX_VERIFICATION_ATTEMPTS = 5
|
||||
|
||||
EMAIL_REGEX = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
|
||||
|
||||
@@ -246,12 +248,14 @@ def _validate_password(password: str):
|
||||
def _set_login_cookies(response: Response, user_id: str):
|
||||
"""设置登录 Cookie"""
|
||||
max_age = settings.SESSION_EXPIRE_MINUTES * 60
|
||||
session_token = create_session_token(user_id, max_age)
|
||||
response.set_cookie(
|
||||
key="user_id",
|
||||
value=user_id,
|
||||
key="session_token",
|
||||
value=session_token,
|
||||
max_age=max_age,
|
||||
httponly=True,
|
||||
samesite="lax"
|
||||
samesite="lax",
|
||||
secure=not settings.debug,
|
||||
)
|
||||
|
||||
china_now = get_china_now()
|
||||
@@ -263,12 +267,13 @@ def _set_login_cookies(response: Response, user_id: str):
|
||||
value=str(expire_at),
|
||||
max_age=max_age,
|
||||
httponly=False,
|
||||
samesite="lax"
|
||||
samesite="lax",
|
||||
secure=not settings.debug,
|
||||
)
|
||||
|
||||
|
||||
def _generate_verification_code() -> str:
|
||||
return f"{random.randint(0, 999999):06d}"
|
||||
return f"{secrets.randbelow(1000000):06d}"
|
||||
|
||||
|
||||
def _build_verification_mail_content(scene: str, code: str, ttl_minutes: int) -> tuple[str, str, str]:
|
||||
@@ -456,6 +461,7 @@ async def send_email_verification_code(request: EmailSendCodeRequest):
|
||||
"code": code,
|
||||
"expires_at": expires_at,
|
||||
"last_sent_at": now,
|
||||
"attempts": 0,
|
||||
}
|
||||
|
||||
logger.info(f"[邮箱验证码] 场景={scene} 已发送到 {email}")
|
||||
@@ -493,6 +499,10 @@ async def email_register(request: EmailRegisterRequest, response: Response):
|
||||
raise HTTPException(status_code=400, detail="验证码已过期,请重新发送")
|
||||
|
||||
if cached["code"] != code:
|
||||
cached["attempts"] = cached.get("attempts", 0) + 1
|
||||
if cached["attempts"] >= MAX_VERIFICATION_ATTEMPTS:
|
||||
_email_verification_storage.pop(_get_verification_storage_key("register", email), None)
|
||||
raise HTTPException(status_code=429, detail="验证码错误次数过多,请重新发送")
|
||||
raise HTTPException(status_code=400, detail="验证码错误")
|
||||
|
||||
existing_user = await _find_user_by_email(email)
|
||||
@@ -541,6 +551,10 @@ async def email_login(request: EmailLoginRequest, response: Response):
|
||||
raise HTTPException(status_code=400, detail="登录验证码已过期,请重新发送")
|
||||
|
||||
if cached["code"] != code:
|
||||
cached["attempts"] = cached.get("attempts", 0) + 1
|
||||
if cached["attempts"] >= MAX_VERIFICATION_ATTEMPTS:
|
||||
_email_verification_storage.pop(storage_key, None)
|
||||
raise HTTPException(status_code=429, detail="验证码错误次数过多,请重新发送")
|
||||
raise HTTPException(status_code=400, detail="登录验证码错误")
|
||||
|
||||
_email_verification_storage.pop(storage_key, None)
|
||||
@@ -588,6 +602,10 @@ async def email_reset_password(request: EmailResetPasswordRequest):
|
||||
raise HTTPException(status_code=400, detail="重置密码验证码已过期,请重新发送")
|
||||
|
||||
if cached["code"] != code:
|
||||
cached["attempts"] = cached.get("attempts", 0) + 1
|
||||
if cached["attempts"] >= MAX_VERIFICATION_ATTEMPTS:
|
||||
_email_verification_storage.pop(storage_key, None)
|
||||
raise HTTPException(status_code=429, detail="验证码错误次数过多,请重新发送")
|
||||
raise HTTPException(status_code=400, detail="重置密码验证码错误")
|
||||
|
||||
await password_manager.set_password(user.user_id, email, request.new_password)
|
||||
@@ -757,6 +775,7 @@ async def logout(request: Request, response: Response):
|
||||
logger.info(f"🚪 [退出] 用户 {user_id} 退出登录")
|
||||
|
||||
response.delete_cookie("user_id")
|
||||
response.delete_cookie("session_token")
|
||||
response.delete_cookie("session_expire_at")
|
||||
return {"message": "退出登录成功"}
|
||||
|
||||
@@ -782,8 +801,6 @@ async def get_password_status(request: Request):
|
||||
username = await password_manager.get_username(user.user_id)
|
||||
|
||||
default_password = None
|
||||
if has_password and not has_custom:
|
||||
default_password = f"{user.username}@666"
|
||||
|
||||
return PasswordStatusResponse(
|
||||
has_password=has_password,
|
||||
|
||||
Reference in New Issue
Block a user