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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user