From d58b706799670990aeead81b50ce6f05adb80d69 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 5 May 2026 22:49:57 +0800 Subject: [PATCH] security: implement Phase 1 & partial Phase 2 improvements - Remove hardcoded admin password, use env var or auto-generate - Fix trustHost to be conditional (Vercel-only by default) - Add HTTP security headers (CSP, X-Frame-Options, HSTS) - Create middleware.ts to activate existing rate limiting - Implement structured logging system - Rewrite error handling in ai-cad service - Update security documentation --- .env.example | 23 +++ SECURITY_IMPROVEMENTS_SUMMARY.md | 236 +++++++++++++++++++++++++++ app/(auth)/auth.config.ts | 8 +- app/(auth)/auth.ts | 1 - create-admin.ts | 39 ++++- lib/logger.ts | 176 ++++++++++++++++++++ lib/security/IMPLEMENTATION_GUIDE.md | 216 +++++++++--------------- middleware.ts | 3 + next.config.ts | 40 +++++ services/ai-cad/index.ts | 62 ++++--- services/canvas/index.ts | 12 ++ 11 files changed, 651 insertions(+), 165 deletions(-) create mode 100644 SECURITY_IMPROVEMENTS_SUMMARY.md create mode 100644 lib/logger.ts create mode 100644 middleware.ts diff --git a/.env.example b/.env.example index ac14a3f53..5a0a72db9 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,25 @@ # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` AUTH_SECRET=**** +# Auth URL (optional in Vercel, required for self-hosted) +# Set to your application's public URL, e.g. https://your-domain.com +# Also accepted as NEXTAUTH_URL (deprecated, prefer AUTH_URL) +AUTH_URL= + +# 仅在受信反向代理(如 Vercel、Nginx with validated Host header)后部署时设为 true +# 开启后禁用 next-auth 的 Host 头校验,非受信环境不要开启 +AUTH_TRUST_HOST=false + +# Google OAuth (optional) +# https://console.cloud.google.com/apis/credentials +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# Admin user credentials (used by create-admin.ts one-time setup script) +# Set both to provision the initial admin account; if omitted, auto-generated +ADMIN_EMAIL= +ADMIN_PASSWORD= + # The following keys below are automatically created and # added to your environment when you deploy on Vercel @@ -48,3 +67,7 @@ TRIPO_BASE_URL=https://api.tripo3d.ai/v2/openapi # 优先使用系统设置-本地AI里的 `kittycad-cad` 提供商;若未配置则回退到环境变量 KITTYCAD_API_TOKEN=your-kittycad-api-token KITTYCAD_BASE_URL=https://api.zoo.dev + +# FAL.ai(可选) +# 用于图片生成和编辑;若未配置则回退到系统设置中的 AI 提供商 +FAL_KEY=your-fal-ai-key diff --git a/SECURITY_IMPROVEMENTS_SUMMARY.md b/SECURITY_IMPROVEMENTS_SUMMARY.md new file mode 100644 index 000000000..ee74ff3d0 --- /dev/null +++ b/SECURITY_IMPROVEMENTS_SUMMARY.md @@ -0,0 +1,236 @@ +# 安全改进总结 + +**日期**: 2026-05-05 +**改进范围**: Phase 1 (安全短板) + Phase 2 (部分工程基础) + +--- + +## 已完成的改进 + +### Phase 1: 安全短板修复 ✅ + +#### 1. 修复硬编码管理员密码 (create-admin.ts) +**风险等级**: 🟢 低风险 +**改动**: +- 从环境变量 `ADMIN_EMAIL` / `ADMIN_PASSWORD` 读取凭据 +- 如未设置则自动生成安全随机密码 +- 不再将密码打印到 console(仅在自动生成时打印一次) + +**向后兼容**: ✅ 是(未设置环境变量时使用默认邮箱 + 随机密码) + +**测试建议**: +```bash +# 测试 1: 使用环境变量 +ADMIN_EMAIL=admin@test.com ADMIN_PASSWORD=SecurePass123! node create-admin.ts + +# 测试 2: 自动生成密码 +node create-admin.ts +``` + +--- + +#### 2. 修复 trustHost 安全配置 +**风险等级**: 🟡 中等风险 +**改动**: +- `app/(auth)/auth.config.ts`: `trustHost` 改为条件启用 + - Vercel 环境 (`VERCEL=1`) 自动开启 + - 其他环境需显式设置 `AUTH_TRUST_HOST=true` +- `app/(auth)/auth.ts`: 移除重复的 `trustHost: true` 覆盖 + +**潜在影响**: +- ⚠️ 非 Vercel 部署且未设置 `AUTH_TRUST_HOST=true` 时,NextAuth 会严格校验 Host 头 +- 如果反向代理未正确传递 Host 头,可能导致登录失败 + +**测试建议**: +```bash +# 开发环境测试登录流程 +pnpm dev +# 访问 http://localhost:3000/login 测试登录 + +# 生产环境部署前检查 +echo $VERCEL # Vercel 环境应为 "1" +echo $AUTH_TRUST_HOST # 非 Vercel 环境应设为 "true" +``` + +--- + +#### 3. 添加 HTTP 安全头 +**风险等级**: 🟢 低风险 +**改动**: `next.config.ts` 新增 `headers()` 函数 +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: SAMEORIGIN` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Permissions-Policy: camera=(), microphone=(), geolocation=()` +- `Content-Security-Policy` (开发环境允许 unsafe-eval,生产环境更严格) + +**潜在影响**: +- CSP 可能阻止某些第三方脚本(如未在 `connect-src` 白名单中的 API) +- 如有嵌入需求(iframe),需调整 `X-Frame-Options` + +**测试建议**: +```bash +# 启动后检查响应头 +curl -I http://localhost:3000 | grep -E "X-Content-Type|X-Frame|CSP" +``` + +--- + +#### 4. 创建 middleware.ts 接入 proxy.ts +**风险等级**: 🔴 **高风险** - 需重点测试 +**改动**: 新建 `/middleware.ts` +```typescript +export { proxy as middleware, config } from "./proxy"; +``` + +**影响**: +- ✅ 限流功能**首次生效**(之前 proxy.ts 存在但未被 Next.js 加载) +- ✅ IP 黑名单检查生效 +- ✅ 可疑请求检测生效 +- ⚠️ 所有 API 路由现在都会经过中间件检查 + +**潜在问题**: +1. **限流过严**: 如果用户正常使用触发限流(如画布自动保存频繁),会返回 429 +2. **误判可疑请求**: `lib/security/request-monitor.ts` 的规则可能误报 +3. **性能影响**: 每个请求都要经过 IP 检查、限流检查、可疑模式匹配 + +**测试建议**: +```bash +# 1. 检查中间件是否加载 +pnpm build +cat .next/server/middleware-manifest.json # 应有内容,不再为空 + +# 2. 测试限流 +# 快速刷新页面 100 次,应触发 429 + +# 3. 测试正常流程 +# 登录、创建画布、上传图片、聊天 - 确保都正常工作 + +# 4. 检查日志 +# 查看是否有 "[Security] Rate limit exceeded" 等日志 +``` + +--- + +#### 5. 补充 .env.example +**风险等级**: 🟢 无风险(纯文档) +**新增条目**: +- `AUTH_URL` / `NEXTAUTH_URL` +- `AUTH_TRUST_HOST` +- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` +- `ADMIN_EMAIL` / `ADMIN_PASSWORD` +- `FAL_KEY` + +--- + +### Phase 2: 工程基础改进 ✅ + +#### 6. 建立结构化日志系统 +**风险等级**: 🟢 低风险 +**新增文件**: `lib/logger.ts` +- 提供 `log.info()`, `log.error()`, `log.warn()`, `log.debug()` +- 支持模块化日志:`createLogger("ModuleName")` +- 开发环境可读格式,生产环境 JSON 格式 +- 通过 `LOG_LEVEL` 环境变量控制输出级别 + +**使用示例**: +```typescript +import { createLogger } from "@/lib/logger"; +const log = createLogger("MyService"); +log.error("Operation failed", error, { userId: "123" }); +``` + +--- + +#### 7. 修复 service 层错误吞没 +**风险等级**: 🟡 中等风险 +**改动**: +- `services/ai-cad/index.ts`: 完全重写,所有 catch 块现在记录错误 +- `services/canvas/index.ts`: 添加 logger 导入和辅助函数(未全部迁移) + +**潜在问题**: +- `handleError(err, context, fallbackMessage)` 参数顺序与之前不同 +- 如果有其他代码依赖 ai-cad service 的错误格式,可能受影响 + +**测试建议**: +```bash +# 触发一个 AI-CAD 错误(如无效的 workspace ID) +# 检查日志是否包含完整错误堆栈 +``` + +--- + +#### 8. 更新安全文档 +**风险等级**: 🟢 无风险(纯文档) +**改动**: `lib/security/IMPLEMENTATION_GUIDE.md` 更新至实际状态 + +--- + +## 未完成的任务 + +### Phase 2 剩余 +- ❌ 为缺少验证的路由补充 Zod schema (enhance, rmbg, canvas/text-edit 等) +- ❌ 迁移剩余 service 文件的错误处理(canvas 有 31 个 catch 块未完全迁移) + +### Phase 3 +- ❌ 补充核心单元测试 +- ❌ 添加 Dockerfile 支持 + +--- + +## 回滚方案 + +如果部署后出现问题,可以快速回滚: + +### 回滚 middleware(最高优先级) +```bash +# 删除 middleware.ts,限流和安全检查将停止工作 +rm middleware.ts +pnpm build +``` + +### 回滚 trustHost +```bash +# 在 auth.config.ts 中改回 +trustHost: true, +``` + +### 回滚其他改动 +```bash +git diff HEAD # 查看所有改动 +git checkout HEAD -- # 回滚单个文件 +``` + +--- + +## 部署前检查清单 + +- [ ] 在开发环境运行 `pnpm dev`,确保无编译错误 +- [ ] 测试登录/登出流程 +- [ ] 测试聊天、画布、图片生成等核心功能 +- [ ] 快速刷新页面,确认限流触发(应看到 429 错误) +- [ ] 检查 `.next/server/middleware-manifest.json` 不为空 +- [ ] 设置生产环境变量: + - `AUTH_TRUST_HOST=true`(如非 Vercel) + - `LOG_LEVEL=warn`(生产环境) + - `ADMIN_PASSWORD=`(如需创建管理员) +- [ ] 运行 `pnpm build` 确保生产构建成功 + +--- + +## 监控建议 + +部署后前 24 小时重点监控: + +1. **错误率**: 是否有大量 429 (限流) 或 403 (IP 黑名单) 错误 +2. **登录成功率**: trustHost 改动可能影响登录 +3. **日志输出**: 检查是否有 "[Security]" 前缀的警告 +4. **性能**: 中间件增加的延迟(应 <10ms) + +--- + +## 联系与支持 + +如遇问题,优先检查: +1. `.next/server/middleware-manifest.json` 是否正确生成 +2. 浏览器控制台是否有 CSP 违规警告 +3. 服务器日志中是否有 "[Security]" 相关错误 diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index 64bb6fb54..503ae9876 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -6,8 +6,14 @@ export function buildAuthConfig(authUrl?: string | null): NextAuthConfig { // Use host-only cookies to avoid cross-domain login issues in multi-domain deployments. const cookieDomain = undefined; + // trustHost 仅在明确配置时开启,避免 host header 注入攻击 + // 部署在 Vercel 或受信反向代理后时,设置 AUTH_TRUST_HOST=true + const trustHost = + process.env.AUTH_TRUST_HOST === "true" || + process.env.VERCEL === "1"; + return { - trustHost: true, + trustHost, pages: { signIn: "/login", newUser: "/", diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index ee6c9eaaa..296de6639 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -211,7 +211,6 @@ const nextAuth = NextAuth(async () => { return { ...buildAuthConfig(resolvedAuthUrl), - trustHost: true, providers: createBaseProviders(googleClientId, googleClientSecret), callbacks: { async signIn({ account, user, profile }) { diff --git a/create-admin.ts b/create-admin.ts index fb7984f1c..31a06cc59 100644 --- a/create-admin.ts +++ b/create-admin.ts @@ -1,9 +1,21 @@ +import crypto from "node:crypto"; import fs from "node:fs"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import { user } from "./lib/db/schema"; import { generateHashedPassword } from "./lib/db/utils"; +function generateSecurePassword(length = 16): string { + const charset = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; + const bytes = crypto.randomBytes(length); + let password = ""; + for (let i = 0; i < length; i++) { + password += charset[bytes[i] % charset.length]; + } + return password; +} + async function main() { let connectionString = process.env.POSTGRES_URL; @@ -28,8 +40,15 @@ async function main() { const client = postgres(connectionString); const db = drizzle(client); - const email = "admin@example.com"; - const password = "Admin@123456"; + // 从环境变量读取管理员邮箱和密码,避免硬编码凭据 + const email = + process.env.ADMIN_EMAIL || + process.env.INIT_ADMIN_EMAIL || + "admin@example.com"; + const password = + process.env.ADMIN_PASSWORD || + process.env.INIT_ADMIN_PASSWORD || + generateSecurePassword(); const hashedPassword = generateHashedPassword(password); try { @@ -41,11 +60,19 @@ async function main() { }); // biome-ignore lint/style/noConsole: 仅用于一次性脚本 - console.log("Admin user created:", email, "password:", password); - } catch (error) { + console.log("Admin user created:", email); + if (!process.env.ADMIN_PASSWORD && !process.env.INIT_ADMIN_PASSWORD) { + console.log( + "Auto-generated password (save this - it will not be shown again):", + password + ); + } + } catch (error: unknown) { + const message = + error instanceof Error ? error.message : String(error); // 如果已存在则忽略错误 // biome-ignore lint/style/noConsole: 仅用于一次性脚本 - console.log("Admin user may already exist:", email, error); + console.log("Admin user may already exist:", email, "-", message); } await client.end(); @@ -53,7 +80,7 @@ async function main() { main().catch((error) => { // biome-ignore lint/style/noConsole: 仅用于一次性脚本 - console.error(error); + console.error("Failed to create admin user:", error); process.exit(1); }); diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 000000000..75fdb5346 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,176 @@ +/** + * 结构化日志系统 + * + * 替代散布在代码中的 console.log/error/warn,提供: + * - 统一的日志格式 (JSON) + * - 日志级别过滤 + * - 请求关联 ID 追踪 + * - 错误堆栈保留 + * + * 用法: + * import { logger, log } from "@/lib/logger"; + * log.info("User logged in", { userId: "123" }); + * log.error("Payment failed", error, { orderId: "456" }); + */ + +type LogLevel = "debug" | "info" | "warn" | "error"; + +interface LogEntry { + level: LogLevel; + message: string; + timestamp: string; + error?: { + name: string; + message: string; + stack?: string; + }; + [key: string]: unknown; +} + +type LogArgs = [string] | [string, Record] | [string, Error, Record?]; + +// 生产环境只输出 warn 及以上级别 +const MIN_LEVEL: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; + +function resolveMinLevel(): LogLevel { + const configured = process.env.LOG_LEVEL as LogLevel | undefined; + if (configured && configured in MIN_LEVEL) return configured; + return process.env.NODE_ENV === "production" ? "warn" : "info"; +} + +const currentMinLevel = resolveMinLevel(); + +function shouldLog(level: LogLevel): boolean { + return MIN_LEVEL[level] >= MIN_LEVEL[currentMinLevel]; +} + +function formatError(err: Error): LogEntry["error"] { + return { + name: err.name, + message: err.message, + ...(err.stack ? { stack: err.stack.split("\n").slice(0, 5).join("\n") } : {}), + }; +} + +function createEntry( + level: LogLevel, + message: string, + extra?: Record, + err?: Error +): LogEntry { + const entry: LogEntry = { + level, + message, + timestamp: new Date().toISOString(), + }; + + if (err) { + entry.error = formatError(err); + } + + if (extra) { + // Skip internal fields to avoid overwriting + for (const [key, value] of Object.entries(extra)) { + if (key !== "level" && key !== "message" && key !== "timestamp" && key !== "error") { + entry[key] = value; + } + } + } + + return entry; +} + +function formatEntry(entry: LogEntry): string { + // 开发环境使用可读格式,生产环境使用 JSON + if (process.env.NODE_ENV !== "production") { + const parts = [`[${entry.timestamp}]`, `[${entry.level.toUpperCase()}]`, entry.message]; + if (entry.error) { + parts.push(`| ${entry.error.name}: ${entry.error.message}`); + } + // 额外字段 + const extraKeys = Object.keys(entry).filter( + (k) => k !== "level" && k !== "message" && k !== "timestamp" && k !== "error" + ); + if (extraKeys.length > 0) { + const extra: Record = {}; + for (const k of extraKeys) extra[k] = entry[k]; + parts.push(JSON.stringify(extra)); + } + return parts.join(" "); + } + return JSON.stringify(entry); +} + +function write(entry: LogEntry): void { + const formatted = formatEntry(entry); + switch (entry.level) { + case "error": + console.error(formatted); + break; + case "warn": + console.warn(formatted); + break; + case "info": + console.info(formatted); + break; + case "debug": + console.debug(formatted); + break; + } +} + +// --- 公共 API --- + +function logFn(level: LogLevel) { + return (...args: LogArgs): void => { + if (!shouldLog(level)) return; + + const [message, arg2, arg3] = args; + let extra: Record | undefined; + let err: Error | undefined; + + if (arg2 instanceof Error) { + err = arg2; + extra = arg3; + } else if (arg2 && typeof arg2 === "object") { + extra = arg2; + } + + write(createEntry(level, message, extra, err)); + }; +} + +export const log = { + debug: logFn("debug"), + info: logFn("info"), + warn: logFn("warn"), + error: logFn("error"), +}; + +/** + * 带模块名的日志工厂 + * + * 用法: + * const log = createLogger("EmailVerification"); + * log.info("Code sent", { email: "user@example.com" }); + */ +export function createLogger(module: string) { + return { + debug: (message: string, extra?: Record) => + log.debug(message, { ...extra, module }), + info: (message: string, extra?: Record) => + log.info(message, { ...extra, module }), + warn: (message: string, extra?: Record) => + log.warn(message, { ...extra, module }), + error: (message: string, err?: Error, extra?: Record) => + log.error(message, err, { ...extra, module }), + }; +} + +export { createLogger as logger }; +export default log; diff --git a/lib/security/IMPLEMENTATION_GUIDE.md b/lib/security/IMPLEMENTATION_GUIDE.md index 8a79aa0fd..428acd16d 100644 --- a/lib/security/IMPLEMENTATION_GUIDE.md +++ b/lib/security/IMPLEMENTATION_GUIDE.md @@ -1,159 +1,105 @@ # 安全加固实施指南 -## 🎯 解决方案概述 +## 当前状态 -本方案采用**多层防护**策略,不依赖IP黑名单,而是基于**行为模式检测**来阻止攻击。 +本项目已实施多层安全防护。以下为实际部署情况: -## 📋 已实现的安全功能 +## 已实现的安全功能 -### 1. ✅ 速率限制(Rate Limiting) +### 1. 速率限制(Rate Limiting) - **文件**:`lib/security/rate-limiter.ts` +- **中间件入口**:`middleware.ts` → `proxy.ts` - **功能**: - - API请求:每分钟100次 - - 认证请求:每分钟5次 - - 严格模式:每分钟10次 -- **优势**:自动阻止异常请求,不依赖IP黑名单 + - 聊天接口:每分钟 60 次 + - 认证接口:每分钟 5 次 + - 文件上传:每分钟 60 次 + - 图片代理:每分钟 3000 次 + - 画布保存:每分钟 300 次 + - 实时画布:每分钟 600 次 + - UI 轮询:每分钟 1200 次 + - 通用 API:每分钟 100 次 +- **注意**:当前使用内存存储,多实例部署时建议迁移至 Redis(见下方中长期建议) -### 2. ✅ 可疑请求检测 +### 2. 可疑请求检测 - **文件**:`lib/security/request-monitor.ts` - **功能**: - - 检测可疑路径(/.env, /.git等) - - 检测可疑User-Agent - - 记录所有可疑请求 -- **优势**:基于行为模式,适用于所有攻击者 + - 检测可疑路径(/.env、/.git 等) + - 检测可疑 User-Agent + - 记录但不立即阻止(避免误报) +- **状态**:已实现并通过中间件自动运行 -### 3. ✅ 输入清理和验证 +### 3. 输入清理和验证 - **文件**:`lib/security/input-sanitizer.ts` - **功能**: - - 移除命令注入字符(; | ` $等) - - 移除危险关键词(wget, curl, bash等) - - 防止路径遍历(../) - - 防止XSS攻击 -- **优势**:在数据进入应用前就清理,防止命令注入 + - 移除命令注入字符 + - 移除危险关键词 + - 防止路径遍历 + - 防止 XSS 攻击 +- **状态**:工具函数已就绪,需在各 API 路由中逐步接入 -### 4. ✅ 中间件集成 -- **文件**:`middleware.ts` +### 4. IP 黑名单 +- **文件**:`lib/security/ip-blocklist.ts` +- **功能**:基于配置的 IP 黑名单拦截 +- **状态**:已通过中间件自动运行 + +### 5. HTTP 安全头 +- **文件**:`next.config.ts` (headers 配置) +- **已添加**: + - `X-Content-Type-Options: nosniff` + - `X-Frame-Options: SAMEORIGIN` + - `Referrer-Policy: strict-origin-when-cross-origin` + - `Permissions-Policy: camera=(), microphone=(), geolocation=()` + - `Content-Security-Policy`(根据开发/生产环境自动调整) + +### 6. 认证安全 +- **文件**:`app/(auth)/auth.config.ts`, `app/(auth)/auth.ts` +- **改进**: + - `trustHost` 已改为条件启用(Vercel 环境自动开启,其他环境需设置 + `AUTH_TRUST_HOST=true`) + - Session cookie 使用 `httpOnly + sameSite: lax + secure`(HTTPS 环境) + - 单会话强制(regular 用户) + - bcrypt 密码哈希 + 时序攻击缓解 + +### 7. 结构化日志 +- **文件**:`lib/logger.ts` - **功能**: - - 自动应用速率限制 - - 检测可疑请求 - - 记录安全事件 -- **优势**:所有请求都经过安全检查 + - JSON 格式输出(生产环境) + - 日志级别过滤(`LOG_LEVEL` 环境变量) + - 模块名标记(`createLogger("ModuleName")`) + - 错误堆栈保留 +- **用法**: + ```typescript + import { createLogger } from "@/lib/logger"; + const log = createLogger("MyService"); + log.error("Operation failed", error, { userId: "123" }); + ``` -## 🚀 实施步骤 +## 待改进项 -### 步骤1:部署代码更新 -```bash -# 1. 提交代码更改 -git add lib/security/ middleware.ts scripts/ -git commit -m "feat: 添加多层安全防护机制" -git push +### 短期(1-2 周) +1. 在所有 API 路由中接入 Zod 输入验证 +2. 将现有 `console.error` 调用迁移至结构化日志 +3. 为 service 层 catch 块添加错误日志记录 -# 2. 在服务器上拉取更新 -cd /var/www/ai-chatbot -git pull - -# 3. 重新构建 -pnpm install -pnpm build - -# 4. 重启应用 -pm2 restart ai-chatbot -``` - -### 步骤2:安装fail2ban(SSH防护) -```bash -# 在服务器上执行 -bash scripts/install-fail2ban.sh - -# 或手动安装 -dnf install -y epel-release fail2ban -systemctl enable fail2ban -systemctl start fail2ban -``` - -### 步骤3:在API路由中应用输入验证 -在关键的API路由中添加输入验证: - -```typescript -import { validateRequestBody } from "@/lib/security/input-sanitizer"; - -export async function POST(request: Request) { - const body = await request.json(); - - // 验证和清理请求体 - const validation = await validateRequestBody(body); - if (!validation.valid) { - return NextResponse.json( - { error: "Invalid request", details: validation.errors }, - { status: 400 } - ); - } - - // 使用清理后的数据 - const sanitizedBody = validation.sanitized; - // ... 处理请求 -} -``` - -## 🔍 监控和日志 - -### 查看安全日志 -```bash -# 查看应用日志中的安全事件 -tail -f /var/log/pm2/ai-chatbot-out.log | grep "Security" - -# 查看fail2ban日志 -tail -f /var/log/fail2ban.log - -# 查看被封禁的IP -fail2ban-client status sshd -``` - -### 监控指标 -- 速率限制触发次数 -- 可疑请求检测次数 -- 输入验证失败次数 -- fail2ban封禁的IP数量 - -## 🛡️ 防护效果 - -### 防止的攻击类型 -1. ✅ **命令注入**:通过输入清理和验证 -2. ✅ **暴力破解**:通过速率限制和fail2ban -3. ✅ **DDoS攻击**:通过速率限制 -4. ✅ **路径遍历**:通过输入清理 -5. ✅ **XSS攻击**:通过输入清理 -6. ✅ **异常请求**:通过可疑请求检测 - -### 优势 -- 🌍 **不依赖IP黑名单**:适用于所有攻击者 -- 🔄 **自动适应**:基于行为模式检测 -- 📊 **可监控**:详细的安全日志 -- ⚡ **高性能**:轻量级实现,不影响性能 - -## 📚 进一步优化建议 - -### 短期(1-2周) -1. 在所有API路由中应用输入验证 -2. 配置fail2ban监控更多服务(Nginx、PostgreSQL等) -3. 设置安全告警(邮件/短信通知) - -### 中期(1-2月) -1. 集成CrowdSec获取社区威胁情报 +### 中期(1-2 月) +1. 将内存速率限制迁移至 Redis(`@upstash/ratelimit`) 2. 实施更细粒度的速率限制策略 -3. 添加请求签名验证 +3. 添加结构化日志的持久化存储 +4. 集成错误追踪服务(Sentry 等) -### 长期(3-6月) -1. 部署ModSecurity WAF -2. 实施SELinux策略 +### 长期(3-6 月) +1. 部署 WAF +2. 实施 SELinux 策略 3. 建立安全审计流程 +4. CI/CD 中集成 SAST 和依赖扫描 -## 🆘 应急响应 +## 环境变量 -如果发现新的攻击: -1. 检查应用日志:`tail -f /var/log/pm2/ai-chatbot-error.log` -2. 检查fail2ban状态:`fail2ban-client status` -3. 手动封禁IP:`fail2ban-client set sshd banip ` -4. 更新可疑模式:编辑 `lib/security/request-monitor.ts` -5. 更新输入清理规则:编辑 `lib/security/input-sanitizer.ts` +新增安全相关环境变量(详见 `.env.example`): +| 变量 | 说明 | 默认值 | +|---|---|---| +| `AUTH_TRUST_HOST` | 是否信任代理 Host 头(非受信环境不要开启) | `false` | +| `LOG_LEVEL` | 日志级别(debug/info/warn/error) | 开发: info, 生产: warn | +| `ADMIN_EMAIL` | 初始管理员邮箱(`create-admin.ts`) | `admin@example.com` | +| `ADMIN_PASSWORD` | 初始管理员密码(不设置则自动生成) | 自动生成 | diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 000000000..f5bfc0d82 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,3 @@ +// Next.js 中间件入口 — 将 proxy.ts 的安全逻辑正确接入 +// 之前 proxy.ts 存在但未正确导出为 Next.js middleware 格式,导致限流和安全检查均未生效 +export { proxy as middleware, config } from "./proxy"; diff --git a/next.config.ts b/next.config.ts index 432498875..b96bf8e17 100644 --- a/next.config.ts +++ b/next.config.ts @@ -35,6 +35,46 @@ const nextConfig: NextConfig = { }, ], }, + // HTTP 安全头 + async headers() { + const isDev = process.env.NODE_ENV === "development"; + const csp = [ + "default-src 'self'", + // Next.js 需要 unsafe-eval 用于开发模式 (Fast Refresh / Turbopack) + `script-src 'self'${isDev ? " 'unsafe-eval'" : ""}`, + // Next.js 需要 unsafe-inline 用于样式 (CSS-in-JS / Tailwind) + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob: https: http:", + "font-src 'self' data:", + "connect-src 'self' https: wss:", + "media-src 'self' blob: https:", + "frame-src 'self'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + ].join("; "); + + return [ + { + source: "/(.*)", + headers: [ + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, + { key: "Content-Security-Policy", value: csp }, + ], + }, + // 对需要嵌入的页面放宽 frame 限制 + { + source: "/worlds/share/:path*", + headers: [ + { key: "X-Frame-Options", value: "ALLOW-FROM https://www.risesail.com" }, + ], + }, + ]; + }, + async redirects() { return [ { diff --git a/services/ai-cad/index.ts b/services/ai-cad/index.ts index 8504cd1f3..42dc5e421 100644 --- a/services/ai-cad/index.ts +++ b/services/ai-cad/index.ts @@ -1,4 +1,7 @@ import type { Model3DTaskStatus } from "@/services/canvas"; +import { createLogger } from "@/lib/logger"; + +const log = createLogger("AiCad"); type ApiResponse = { success: boolean; @@ -53,14 +56,25 @@ export type AiCadModelingCommandPayload = { raw?: any; }; +function handleError( + err: unknown, + context: string, + fallbackMessage: string, + extra?: Record +): ApiResponse { + const error = err instanceof Error ? err : new Error(String(err)); + log.error(`${context} failed`, error, extra); + return { success: false, error: fallbackMessage }; +} + export const aiCadGenerationService = { async listWorkspaces(): Promise> { try { const response = await fetch("/api/ai-cad/workspaces"); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "读取 AI-CAD 工作台列表失败" }; + } catch (err) { + return handleError(err, "listWorkspaces", "读取 AI-CAD 工作台列表失败"); } }, @@ -75,8 +89,8 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "创建 AI-CAD 工作台失败" }; + } catch (err) { + return handleError(err, "createWorkspace", "创建 AI-CAD 工作台失败", { name: params?.name }); } }, @@ -87,8 +101,8 @@ export const aiCadGenerationService = { const response = await fetch(`/api/ai-cad/workspaces/${workspaceId}`); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "读取 AI-CAD 工作台失败" }; + } catch (err) { + return handleError(err, "getWorkspace", "读取 AI-CAD 工作台失败", { workspaceId }); } }, @@ -120,8 +134,8 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "保存 AI-CAD 工作台失败" }; + } catch (err) { + return handleError(err, "saveWorkspace", "保存 AI-CAD 工作台失败", { workspaceId }); } }, @@ -134,8 +148,8 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "删除 AI-CAD 工作台失败" }; + } catch (err) { + return handleError(err, "deleteWorkspace", "删除 AI-CAD 工作台失败", { workspaceId }); } }, @@ -144,8 +158,8 @@ export const aiCadGenerationService = { const response = await fetch("/api/ai-cad/provider"); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "读取 AI-CAD 提供商状态失败" }; + } catch (err) { + return handleError(err, "getProviderStatus", "读取 AI-CAD 提供商状态失败"); } }, @@ -176,8 +190,10 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "创建 AI-CAD 任务失败" }; + } catch (err) { + return handleError(err, "createTask", "创建 AI-CAD 任务失败", { + outputFormat: params.outputFormat, + }); } }, @@ -188,8 +204,8 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "创建官方建模会话失败" }; + } catch (err) { + return handleError(err, "createModelingSession", "创建官方建模会话失败"); } }, @@ -205,8 +221,8 @@ export const aiCadGenerationService = { ); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "关闭官方建模会话失败" }; + } catch (err) { + return handleError(err, "closeModelingSession", "关闭官方建模会话失败", { sessionId }); } }, @@ -225,8 +241,10 @@ export const aiCadGenerationService = { }); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "发送官方建模命令失败" }; + } catch (err) { + return handleError(err, "sendModelingCommand", "发送官方建模命令失败", { + sessionId: params.sessionId, + }); } }, @@ -251,8 +269,8 @@ export const aiCadGenerationService = { const response = await fetch(`/api/ai-cad/tasks/${taskId}`); const data = await response.json(); return data; - } catch (_error) { - return { success: false, error: "查询 AI-CAD 任务失败" }; + } catch (err) { + return handleError(err, "getTask", "查询 AI-CAD 任务失败", { taskId }); } }, }; diff --git a/services/canvas/index.ts b/services/canvas/index.ts index 3780f4338..f1a3e8c74 100644 --- a/services/canvas/index.ts +++ b/services/canvas/index.ts @@ -13,6 +13,18 @@ import type { EnhanceProOutputFormat, EnhanceProSubjectDetection, } from '@/lib/enhance-pro/config'; +import { createLogger } from '@/lib/logger'; + +const log = createLogger("Canvas"); + +function logCatch( + err: unknown, + context: string, + extra?: Record +): void { + const error = err instanceof Error ? err : new Error(String(err)); + log.error(`${context} failed`, error, extra); +} // API 响应类型 interface ApiResponse {