security: implement Phase 1 & partial Phase 2 improvements
Some checks failed
Lint / build (20) (push) Has been cancelled

- 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
This commit is contained in:
Your Name 2026-05-05 22:49:57 +08:00
parent 38c10d616b
commit d58b706799
11 changed files with 651 additions and 165 deletions

View File

@ -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

View File

@ -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 -- <file> # 回滚单个文件
```
---
## 部署前检查清单
- [ ] 在开发环境运行 `pnpm dev`,确保无编译错误
- [ ] 测试登录/登出流程
- [ ] 测试聊天、画布、图片生成等核心功能
- [ ] 快速刷新页面,确认限流触发(应看到 429 错误)
- [ ] 检查 `.next/server/middleware-manifest.json` 不为空
- [ ] 设置生产环境变量:
- `AUTH_TRUST_HOST=true`(如非 Vercel)
- `LOG_LEVEL=warn`(生产环境)
- `ADMIN_PASSWORD=<secure-password>`(如需创建管理员)
- [ ] 运行 `pnpm build` 确保生产构建成功
---
## 监控建议
部署后前 24 小时重点监控:
1. **错误率**: 是否有大量 429 (限流) 或 403 (IP 黑名单) 错误
2. **登录成功率**: trustHost 改动可能影响登录
3. **日志输出**: 检查是否有 "[Security]" 前缀的警告
4. **性能**: 中间件增加的延迟(应 <10ms)
---
## 联系与支持
如遇问题,优先检查:
1. `.next/server/middleware-manifest.json` 是否正确生成
2. 浏览器控制台是否有 CSP 违规警告
3. 服务器日志中是否有 "[Security]" 相关错误

View File

@ -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: "/",

View File

@ -211,7 +211,6 @@ const nextAuth = NextAuth(async () => {
return {
...buildAuthConfig(resolvedAuthUrl),
trustHost: true,
providers: createBaseProviders(googleClientId, googleClientSecret),
callbacks: {
async signIn({ account, user, profile }) {

View File

@ -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);
});

176
lib/logger.ts Normal file
View File

@ -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, unknown>] | [string, Error, Record<string, unknown>?];
// 生产环境只输出 warn 及以上级别
const MIN_LEVEL: Record<LogLevel, number> = {
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<string, unknown>,
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<string, unknown> = {};
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<string, unknown> | 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<string, unknown>) =>
log.debug(message, { ...extra, module }),
info: (message: string, extra?: Record<string, unknown>) =>
log.info(message, { ...extra, module }),
warn: (message: string, extra?: Record<string, unknown>) =>
log.warn(message, { ...extra, module }),
error: (message: string, err?: Error, extra?: Record<string, unknown>) =>
log.error(message, err, { ...extra, module }),
};
}
export { createLogger as logger };
export default log;

View File

@ -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 <IP地址>`
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` | 初始管理员密码(不设置则自动生成) | 自动生成 |

3
middleware.ts Normal file
View File

@ -0,0 +1,3 @@
// Next.js 中间件入口 — 将 proxy.ts 的安全逻辑正确接入
// 之前 proxy.ts 存在但未正确导出为 Next.js middleware 格式,导致限流和安全检查均未生效
export { proxy as middleware, config } from "./proxy";

View File

@ -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 [
{

View File

@ -1,4 +1,7 @@
import type { Model3DTaskStatus } from "@/services/canvas";
import { createLogger } from "@/lib/logger";
const log = createLogger("AiCad");
type ApiResponse<T = any> = {
success: boolean;
@ -53,14 +56,25 @@ export type AiCadModelingCommandPayload = {
raw?: any;
};
function handleError(
err: unknown,
context: string,
fallbackMessage: string,
extra?: Record<string, unknown>
): ApiResponse<never> {
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<ApiResponse<AiCadWorkspaceSummary[]>> {
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 });
}
},
};

View File

@ -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<string, unknown>
): void {
const error = err instanceof Error ? err : new Error(String(err));
log.error(`${context} failed`, error, extra);
}
// API 响应类型
interface ApiResponse<T = any> {