T-005: Security hardening - CORS, Rate Limit, CSRF

- CORS: Restrict allowed origins to specific frontend URLs, limit methods and headers
- Rate Limit: Add fine-grained endpoint-specific rate limits for sensitive operations
  - Login: 5 requests/minute
  - Register: 3 requests/hour
  - Password change: 3 requests/5 minutes
  - Payment: 20 requests/minute
  - Admin: 30 requests/minute
- CSRF: Add CSRF protection middleware with double-submit cookie pattern
  - New app/core/csrf.py module with CSRFMiddleware
  - Require CSRF tokens on sensitive endpoints (auth, payment, profile)
  - Skip webhook endpoints for CSRF validation
- Fix pydantic-settings import in config.py
This commit is contained in:
TradeMate Dev
2026-05-29 10:26:23 +08:00
parent 7c9885f704
commit c04fa2c19f
7 changed files with 464 additions and 58 deletions
+18 -2
View File
@@ -7,11 +7,13 @@ import uuid
from app.database import get_db
from app.models.user import User
from app.core.security import hash_password, verify_password, create_access_token, create_refresh_token, decode_token
from app.core.csrf import require_csrf_token
from pydantic import BaseModel, EmailStr
from datetime import datetime, timedelta
from app.services.admin import AdminService
from app.models.subscription import Subscription
from app.api.v1.referral import apply_referral
from app.config import settings
import logging
logger = logging.getLogger(__name__)
@@ -44,7 +46,12 @@ class RefreshRequest(BaseModel):
@router.post("/register")
async def register(data: RegisterRequest, request: Request, db: AsyncSession = Depends(get_db)):
async def register(
data: RegisterRequest,
request: Request,
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
existing = await db.execute(select(User).where(User.phone == data.phone))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Phone already registered")
@@ -92,6 +99,7 @@ async def login(
data: LoginRequest,
request: Request,
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
login_id = data.username or data.phone
if not login_id:
@@ -269,6 +277,7 @@ async def update_me(
data: ProfileUpdate,
authorization: Optional[str] = Header(None, alias="Authorization"),
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token")
@@ -306,6 +315,7 @@ async def change_password(
data: PasswordChange,
authorization: Optional[str] = Header(None, alias="Authorization"),
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token")
@@ -340,7 +350,12 @@ async def wechat_config():
@router.post("/wechat-login")
async def wechat_login(data: WeChatLoginRequest, request: Request, db: AsyncSession = Depends(get_db)):
async def wechat_login(
data: WeChatLoginRequest,
request: Request,
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
from app.services.wechat import wechat_service
session = await wechat_service.code2session(data.code)
@@ -383,6 +398,7 @@ async def update_settings(
data: SettingsUpdate,
authorization: Optional[str] = Header(None, alias="Authorization"),
db: AsyncSession = Depends(get_db),
_csrf: str = Depends(require_csrf_token),
):
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing token")