fix: security and code quality improvements

Security fixes:
- Add file upload size limits (10MB) for customer and product imports
- Add XLSX file validation with row limits and magic byte checking
- Implement password validation (min 6 chars) in registration
- Add rate limiting for guest login (5 per IP per 15 minutes)
- Sanitize error messages to prevent information leakage
- Fix XSS vulnerability by removing unsafe v-html usage
- Enforce WhatsApp webhook signature verification
- Add SSRF protection with URL validation and IP blocking
- Fix marketing endpoints to use proper authentication

Code quality improvements:
- Create shared utility functions for UUID validation and string sanitization
- Remove duplicate UUID validation code from admin modules
- Remove dead code (pass statement in translation.py)
- Fix aliyun SDK import compatibility
This commit is contained in:
TradeMate Dev
2026-06-11 17:54:07 +08:00
parent d2736d1ef6
commit 13e3992d4c
18 changed files with 272 additions and 48 deletions
+40 -3
View File
@@ -8,7 +8,7 @@ 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 pydantic import BaseModel, EmailStr, field_validator
from datetime import datetime, timedelta
from app.services.admin import AdminService
from app.models.subscription import Subscription
@@ -40,6 +40,13 @@ class LoginRequest(BaseModel):
phone: str = ""
password: str
@field_validator('password')
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 6:
raise ValueError('Password must be at least 6 characters')
return v
class RefreshRequest(BaseModel):
refresh_token: str
@@ -146,6 +153,37 @@ async def login(
@router.post("/login/guest")
async def guest_login(request: Request, db: AsyncSession = Depends(get_db)):
# Rate limiting: max 5 guest logins per IP per 15 minutes
from app.core.redis import get_redis
import time
client_ip = request.client.host if request.client else "unknown"
cache_key = f"guest_login:{client_ip}"
try:
redis_client = await get_redis()
now = int(time.time())
window = 900 # 15 minutes
limit = 5
# Get count of logins in current window
count = await redis_client.get(cache_key)
if count and int(count) >= limit:
raise HTTPException(
status_code=429,
detail="Too many guest login attempts. Please try again later or register an account."
)
# Increment counter
pipe = redis_client.pipeline()
pipe.incr(cache_key)
pipe.expire(cache_key, window)
await pipe.execute()
except Exception:
# If Redis is down, proceed without rate limiting
pass
guest_id = str(uuid.uuid4())
access_token = create_access_token(
{"sub": guest_id, "tier": "guest", "role": "guest", "is_guest": True},
@@ -153,8 +191,7 @@ async def guest_login(request: Request, db: AsyncSession = Depends(get_db)):
)
refresh_token = create_refresh_token({"sub": guest_id, "is_guest": True})
client_ip = request.client.host if request.client else None
await AdminService(db).log_usage(guest_id, "user.login_guest", {}, ip=client_ip)
await AdminService(db).log_usage(guest_id, "user.login_guest", {})
return LoginResponse(
access_token=access_token,