From 8204aff28b541b3463a44787d3eee04790cc063f Mon Sep 17 00:00:00 2001 From: MarkLo Date: Sat, 13 Dec 2025 01:54:47 +0800 Subject: [PATCH] --- backend/app/main.py | 127 +++++++++- .../components/settings/ApiSettingsDialog.tsx | 43 +++- frontend/lib/crypto.ts | 217 ++++++++++++++++++ frontend/lib/storage.ts | 136 ++++++++++- frontend/next.config.ts | 52 +++++ 5 files changed, 548 insertions(+), 27 deletions(-) create mode 100644 frontend/lib/crypto.ts diff --git a/backend/app/main.py b/backend/app/main.py index 9fa1dc77..1e78e2d2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,11 +1,15 @@ """ FastAPI application entry point for TradingAgentsX Backend """ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware import logging import sys +import time from pathlib import Path +from collections import defaultdict +from datetime import datetime, timedelta from backend.app.core.config import settings from backend.app.core.cors import setup_cors @@ -18,6 +22,105 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) + +class SecurityHeadersMiddleware(BaseHTTPMiddleware): + """Add security headers to all responses""" + + async def dispatch(self, request: Request, call_next): + response = await call_next(request) + + # Security headers + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()" + + return response + + +class RateLimitMiddleware(BaseHTTPMiddleware): + """Simple in-memory rate limiting middleware""" + + def __init__(self, app, max_requests: int = 30, window_seconds: int = 60): + super().__init__(app) + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests: dict[str, list[float]] = defaultdict(list) + + async def dispatch(self, request: Request, call_next): + # Skip rate limiting for health checks + if request.url.path == "/api/health": + return await call_next(request) + + # Get client IP + client_ip = request.client.host if request.client else "unknown" + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + client_ip = forwarded_for.split(",")[0].strip() + + # Clean old requests + now = time.time() + cutoff = now - self.window_seconds + self.requests[client_ip] = [ + t for t in self.requests[client_ip] if t > cutoff + ] + + # Check rate limit + if len(self.requests[client_ip]) >= self.max_requests: + retry_after = int(self.window_seconds - (now - self.requests[client_ip][0])) + return JSONResponse( + status_code=429, + content={ + "error": "Too many requests", + "message": f"Rate limit exceeded. Please wait {retry_after} seconds.", + "retry_after": retry_after, + }, + headers={"Retry-After": str(retry_after)}, + ) + + # Record this request + self.requests[client_ip].append(now) + + # Process request + response = await call_next(request) + + # Add rate limit headers + remaining = self.max_requests - len(self.requests[client_ip]) + response.headers["X-RateLimit-Limit"] = str(self.max_requests) + response.headers["X-RateLimit-Remaining"] = str(max(0, remaining)) + response.headers["X-RateLimit-Reset"] = str(int(now + self.window_seconds)) + + return response + + +class SensitiveDataFilter(logging.Filter): + """Filter to mask API keys in log messages""" + + SENSITIVE_PATTERNS = ["api_key", "apikey", "api-key", "token", "secret", "password"] + + def filter(self, record): + if hasattr(record, 'msg') and isinstance(record.msg, str): + msg = record.msg + for pattern in self.SENSITIVE_PATTERNS: + if pattern.lower() in msg.lower(): + # Mask the value after the pattern + import re + msg = re.sub( + rf'({pattern}["\']?\s*[=:]\s*["\']?)([^"\'\s,}}]+)', + r'\1***MASKED***', + msg, + flags=re.IGNORECASE + ) + record.msg = msg + return True + + +# Add sensitive data filter to all loggers +for handler in logging.root.handlers: + handler.addFilter(SensitiveDataFilter()) + + # Create FastAPI application app = FastAPI( title=settings.app_name, @@ -27,6 +130,10 @@ app = FastAPI( redoc_url="/redoc", ) +# Add security middleware (order matters - added first, executed last) +app.add_middleware(SecurityHeadersMiddleware) +app.add_middleware(RateLimitMiddleware, max_requests=30, window_seconds=60) + # Setup CORS setup_cors(app) @@ -47,13 +154,25 @@ async def root(): @app.exception_handler(Exception) async def global_exception_handler(request, exc): - """Global exception handler""" - logger.error(f"Unhandled exception: {str(exc)}", exc_info=True) + """Global exception handler - masks sensitive data in errors""" + error_msg = str(exc) + + # Mask any API keys that might be in the error message + import re + patterns = ["sk-", "sk-ant-", "xai-", "AIza"] + for pattern in patterns: + error_msg = re.sub( + rf'{pattern}[a-zA-Z0-9_-]+', + f'{pattern}***MASKED***', + error_msg + ) + + logger.error(f"Unhandled exception: {error_msg}", exc_info=True) return JSONResponse( status_code=500, content={ "error": "Internal server error", - "detail": str(exc), # Always return detailed error for user debugging + "detail": error_msg, "type": type(exc).__name__, }, ) diff --git a/frontend/components/settings/ApiSettingsDialog.tsx b/frontend/components/settings/ApiSettingsDialog.tsx index 919b9dbf..a0424935 100644 --- a/frontend/components/settings/ApiSettingsDialog.tsx +++ b/frontend/components/settings/ApiSettingsDialog.tsx @@ -29,9 +29,10 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { - getApiSettings, - saveApiSettings, + getApiSettingsAsync, + saveApiSettingsAsync, clearApiSettings, + migrateToEncrypted, type ApiSettings, DEFAULT_API_SETTINGS, } from "@/lib/storage"; @@ -59,25 +60,38 @@ type FormValues = z.infer; export function ApiSettingsDialog() { const [open, setOpen] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); + const [loading, setLoading] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: getApiSettings(), + defaultValues: DEFAULT_API_SETTINGS, }); - // Load settings when dialog opens + // Load and decrypt settings when dialog opens useEffect(() => { if (open) { - const settings = getApiSettings(); - form.reset(settings); + setLoading(true); setSaveSuccess(false); + + // First try to migrate legacy settings + migrateToEncrypted().then(() => { + // Then load decrypted settings + return getApiSettingsAsync(); + }).then((settings) => { + form.reset(settings); + }).catch((error) => { + console.error("Failed to load settings:", error); + }).finally(() => { + setLoading(false); + }); } }, [open, form]); - const onSubmit = (values: FormValues) => { + const onSubmit = async (values: FormValues) => { + setLoading(true); try { - // Type assertion since our form values match ApiSettings structure - saveApiSettings(values as ApiSettings); + // Encrypt and save settings + await saveApiSettingsAsync(values as ApiSettings); setSaveSuccess(true); setTimeout(() => { setSaveSuccess(false); @@ -85,6 +99,8 @@ export function ApiSettingsDialog() { }, 1500); } catch (error) { console.error("Failed to save settings:", error); + } finally { + setLoading(false); } }; @@ -110,7 +126,10 @@ export function ApiSettingsDialog() { API 配置 - 設定您的 API 金鑰。這些資訊會儲存在瀏覽器的本機儲存空間中。 + 設定您的 API 金鑰。這些資訊會以加密形式儲存在瀏覽器中。 + + 🔒 已啟用 AES-256-GCM 加密保護 + @@ -335,8 +354,8 @@ export function ApiSettingsDialog() { )}
-