关于 security
用于审查 Inbox Zero 代码的安全指南技能,涵盖路由认证、按用户限定的数据访问、输入校验、安全错误处理和定时任务保护。
主要功能
- 01选择认证中间件
- 02按用户和账户限定数据
- 03校验资源所有权
- 04输入校验与清理
- 05安全错误处理
- 06定时任务端点认证
适用场景
- 01审查 API 路由的认证方式
- 02检查数据库查询是否限定用户数据
- 03测试资源所有权防护以发现 IDOR
- 04审查输入处理与错误信息泄露
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ elie222/ inbox-zero --skill security安装后在 Claude Code 中输入/security确认已加载。
详细介绍
安全指南
关键安全规则
🚨 绝不能提交绕过这些安全要求的代码。
1. 身份验证与授权中间件
所有处理用户数据的 API 路由都必须使用适当的中间件:
// ✅ CORRECT: Use withEmailAccount for email-scoped operations
export const GET = withEmailAccount(async (request, { params }) => {
const { emailAccountId } = request.auth;
// ...
});
// ✅ CORRECT: Use withAuth for user-scoped operations
export const GET = withAuth(async (request) => {
const { userId } = request.auth;
// ...
});
// ❌ WRONG: Direct access without authentication
export const GET = async (request) => {
// This exposes data to unauthenticated users!
const data = await prisma.user.findMany();
return NextResponse.json(data);
};
2. 数据访问控制
所有数据库查询都必须限定在经过身份验证的用户/账户范围内:
// ✅ CORRECT: Always include user/account filtering
const schedule = await prisma.schedule.findUnique({
where: {
id: scheduleId,
emailAccountId // 🔒 Critical: Ensures user owns this resource
},
});
// ✅ CORRECT: Filter by user ownership
const rules = await prisma.rule.findMany({
where: {
emailAccountId, // 🔒 Only user's rules
enabled: true
},
});
// ❌ WRONG: Missing user/account filtering
const schedule = await prisma.schedule.findUnique({
where: { id: scheduleId }, // 🚨 Any user can access any schedule!
});
3. 资源所有权验证
始终验证资源是否属于经过身份验证的用户:
// ✅ CORRECT: Validate ownership before operations
async function updateRule({ ruleId, emailAccountId, data }) {
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Ownership check
},
});
if (!rule) throw new SafeError("Rule not found"); // Returns 404, doesn't leak existence
return prisma.rule.update({
where: { id: ruleId },
data,
});
}
// ❌ WRONG: Direct updates without ownership validation
async function updateRule({ ruleId, data }) {
return prisma.rule.update({
where: { id: ruleId }, // 🚨 User can modify any rule!
data,
});
}
中间件使用指南
何时使用 withEmailAccount
用于限定到特定邮箱账户的操作:
- 读取/写入邮件、规则、计划等。
- 任何使用
emailAccountId的操作
export const GET = withEmailAccount(async (request) => {
const { emailAccountId, userId, email } = request.auth;
// All three fields available
});
何时使用 withAuth
用于用户级操作:
- 用户设置、API 密钥、推荐
- 仅使用
userId的操作
export const GET = withAuth(async (request) => {
const { userId } = request.auth;
// Only userId available
});
何时仅使用 withError
用于公共端点或自定义身份验证:
- 公共 Webhook(需单独验证)
- 使用自定义身份验证逻辑的端点
- Cron 端点(必须使用
hasCronSecret)
// ✅ CORRECT: Public endpoint with custom auth
export const GET = withError(async (request) => {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
});
// ✅ CORRECT: Cron endpoint with secret validation
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// ... cron logic
});
// ❌ WRONG: Cron endpoint without validation
export const POST = withError(async (request) => {
// 🚨 Anyone can trigger this cron job!
await sendDigestEmails();
});
Cron 端点安全
🚨 关键:未妥善进行身份验证的 Cron 端点可能被任何人触发!
Cron 身份验证模式
// ✅ CORRECT: GET cron endpoint
export const GET = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// Safe to execute cron logic
await processScheduledTasks();
return NextResponse.json({ success: true });
});
// ✅ CORRECT: POST cron endpoint
export const POST = withError(async (request) => {
if (!(await hasPostCronSecret(request))) {
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
// Safe to execute cron logic
await processBulkOperations();
return NextResponse.json({ success: true });
});
Cron 安全检查清单
对于任何执行自动化任务的端点:
- 使用
withError中间件(而非withAuth或withEmailAccount) - 使用
hasCronSecret(request)或hasPostCronSecret(request)验证 Cron 密钥 - 使用
captureException记录未授权尝试 - 对未授权请求返回
401状态码 - 包含批量操作、计划任务或系统维护
常见 Cron 端点模式
// Digest/summary emails
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request: digest"));
return new Response("Unauthorized", { status: 401 });
}
await sendDigestEmails();
});
// Cleanup operations
export const POST = withError(async (request) => {
if (!(await hasPostCronSecret(request))) {
captureException(new Error("Unauthorized cron request: cleanup"));
return new Response("Unauthorized", { status: 401 });
}
await cleanupExpiredData();
});
// System monitoring
export const GET = withError(async (request) => {
if (!hasCronSecret(request)) {
captureException(new Error("Unauthorized cron request: monitor"));
return new Response("Unauthorized", { status: 401 });
}
await monitorSystemHealth();
});
环境设置
确保已正确配置 CRON_SECRET:
# .env.local
CRON_SECRET=your-secure-random-secret-here
⚠️ 绝不要使用可预测的 Cron 密钥,例如:
"secret""password""cron"- 简短或简单的字符串
数据库安全模式
✅ 安全的查询模式
// User-scoped queries
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, email: true } // Only return needed fields
});
// Email account-scoped queries
const emailAccount = await prisma.emailAccount.findUnique({
where: { id: emailAccountId, userId }, // Double validation
});
// Related resource queries with ownership
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId }
},
include: { actions: true }
});
// Filtered list queries
const schedules = await prisma.schedule.findMany({
where: { emailAccountId },
orderBy: { createdAt: 'desc' }
});
❌ 不安全的查询模式
// Missing user scoping
const schedules = await prisma.schedule.findMany(); // 🚨 Returns ALL schedules
// Missing ownership validation
const rule = await prisma.rule.findUnique({
where: { id: ruleId } // 🚨 Can access any user's rule
});
// Exposed sensitive fields
const user = await prisma.user.findUnique({
where: { id: userId }
// 🚨 Returns ALL fields including sensitive data
});
// Direct parameter usage
const userId = request.nextUrl.searchParams.get('userId');
const user = await prisma.user.findUnique({
where: { id: userId } // 🚨 User can access any user by changing URL
});
输入验证与清理
参数验证
// ✅ CORRECT: Validate all inputs
export const GET = withEmailAccount(async (request, { params }) => {
const { id } = await params;
if (!id) {
return NextResponse.json(
{ error: "Missing schedule ID" },
{ status: 400 }
);
}
// Additional validation
if (typeof id !== 'string' || id.length < 10) {
return NextResponse.json(
{ error: "Invalid schedule ID format" },
{ status: 400 }
);
}
});
// ❌ WRONG: Using parameters without validation
export const GET = withEmailAccount(async (request, { params }) => {
const { id } = await params;
// 🚨 Direct usage without validation
const schedule = await prisma.schedule.findUnique({ where: { id } });
});
使用 Zod 验证请求正文
// ✅ CORRECT: Always validate request bodies
const updateRuleSchema = z.object({
name: z.string().min(1).max(100),
enabled: z.boolean(),
conditions: z.array(z.object({
type: z.enum(['FROM', 'SUBJECT', 'BODY']),
value: z.string().min(1)
}))
});
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const validatedData = updateRuleSchema.parse(body); // Throws on invalid data
// Use validatedData, not body
});
错误处理安全
防止信息泄露
// ✅ CORRECT: Safe error responses
if (!rule) {
throw new SafeError("Rule not found"); // Generic 404
}
if (!hasPermission) {
throw new SafeError("Access denied"); // Generic 403
}
// ❌ WRONG: Information disclosure
if (!rule) {
throw new Error(`Rule ${ruleId} does not exist for user ${userId}`);
// 🚨 Reveals internal IDs and logic
}
if (!rule.emailAccountId === emailAccountId) {
throw new Error("This rule belongs to a different account");
// 🚨 Confirms existence of rule and reveals ownership info
}
一致的错误响应
// ✅ CORRECT: Consistent error format
export const GET = withEmailAccount(async (request) => {
try {
// ... operation
} catch (error) {
if (error instanceof SafeError) {
return NextResponse.json(
{ error: error.message, isKnownError: true },
{ status: error.statusCode || 400 }
);
}
// Let middleware handle unexpected errors
throw error;
}
});
常见安全漏洞
1. 不安全的直接对象引用(IDOR)
// ❌ VULNERABLE: User can access any rule by changing ID
export const GET = async (request, { params }) => {
const { ruleId } = await params;
const rule = await prisma.rule.findUnique({ where: { id: ruleId } });
return NextResponse.json(rule);
};
// ✅ SECURE: Always validate ownership
export const GET = withEmailAccount(async (request, { params }) => {
const { emailAccountId } = request.auth;
const { ruleId } = await params;
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Ownership validation
}
});
if (!rule) throw new SafeError("Rule not found");
return NextResponse.json(rule);
});
2. 批量赋值
// ❌ VULNERABLE: User can modify any field
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const rule = await prisma.rule.update({
where: { id: body.id },
data: body // 🚨 User controls all fields, including ownership!
});
});
// ✅ SECURE: Explicitly allow only safe fields
const updateSchema = z.object({
name: z.string(),
enabled: z.boolean(),
// Only allow specific fields
});
export const PUT = withEmailAccount(async (request) => {
const body = await request.json();
const validatedData = updateSchema.parse(body);
const rule = await prisma.rule.update({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // Maintain ownership
},
data: validatedData // Only validated fields
});
});
3. 权限提升
// ❌ VULNERABLE: User can modify admin-only fields
const rule = await prisma.rule.update({
where: { id: ruleId },
data: {
...updateData,
// 🚨 What if updateData contains system fields?
ownerId: 'different-user-id', // User changes ownership!
systemGenerated: false, // User modifies system flags!
}
});
// ✅ SECURE: Whitelist approach
const allowedFields = {
name: updateData.name,
enabled: updateData.enabled,
instructions: updateData.instructions,
// Only explicitly allowed fields
};
const rule = await prisma.rule.update({
where: {
id: ruleId,
emailAccount: { id: emailAccountId }
},
data: allowedFields
});
4. 未受保护的 Cron 端点
// ❌ VULNERABLE: Anyone can trigger cron operations
export const POST = withError(async (request) => {
// 🚨 No authentication - anyone can send digest emails!
await sendDigestEmailsToAllUsers();
return NextResponse.json({ success: true });
});
// ❌ VULNERABLE: Weak cron validation
export const POST = withError(async (request) => {
const body = await request.json();
if (body.secret !== "simple-password") { // 🚨 Predictable secret
return new Response("Unauthorized", { status: 401 });
}
await performSystemMaintenance();
});
// ✅ SECURE: Proper cron authentication
export const POST = withError(async (request) => {
if (!hasCronSecret(request)) { // 🔒 Strong secret validation
captureException(new Error("Unauthorized cron request"));
return new Response("Unauthorized", { status: 401 });
}
await performSystemMaintenance();
});
API 路由安全检查清单
部署任何 API 路由之前,请确认:
身份验证 ✅
- 使用适当的中间件(
withAuth或withEmailAccount) - 或使用带有适当验证的
withError(Cron 端点、Webhook) - Cron 端点使用
hasCronSecret()或hasPostCronSecret() - 不允许公开访问用户数据
- 强制执行会话/令牌验证
授权 ✅
- 所有查询都包含用户/账户筛选条件
- 操作前验证资源所有权
- 不存在未经所有权检查的直接对象引用
输入验证 ✅
- 验证所有参数(类型、格式、长度)
- 请求正文使用 Zod 模式
- 防止 SQL 注入(正确使用 Prisma)
- 查询中不直接使用用户输入
数据保护 ✅
- 仅返回必要字段
- 响应中不暴露敏感数据
- 错误消息不泄露信息
- 错误响应格式保持一致
查询安全 ✅
- 所有
findUnique/findFirst调用都包含所有权筛选条件 - 所有
findMany调用都限定在用户数据范围内 - 不存在返回其他用户数据的查询
- 正确使用 Prisma 关系进行访问控制
代码库中的示例
✅ 良好示例
Frequency API - apps/web/app/api/user/frequency/[id]/route.ts
export const GET = withEmailAccount(async (request, { params }) => {
const emailAccountId = request.auth.emailAccountId;
const { id } = await params;
if (!id) return NextResponse.json({ error: "Missing frequency id" }, { status: 400 });
const schedule = await prisma.schedule.findUnique({
where: { id, emailAccountId }, // 🔒 Scoped to user's account
});
if (!schedule) {
return NextResponse.json({ error: "Schedule not found" }, { status: 404 });
}
return NextResponse.json(schedule);
});
Rules API - apps/web/app/api/user/rules/[id]/route.ts
const rule = await prisma.rule.findUnique({
where: {
id: ruleId,
emailAccount: { id: emailAccountId } // 🔒 Relationship-based ownership check
},
include: { actions: true, categoryFilters: true },
});
安全审查要点
审查代码时,请特别关注:
- 新增 API 路由 - 确保正确使用中间件
- 数据库查询 - 验证用户范围限定
- 参数处理 - 检查验证和清理
- 错误响应 - 确保不会泄露信息
- 批量操作 - 对批量更新/删除格外谨慎
安全测试
手动测试清单
- 绕过身份验证:尝试在不提供身份验证标头的情况下访问端点
- IDOR 测试:修改资源 ID,尝试访问其他用户的数据
- 参数篡改:使用无效/恶意参数进行测试
- 错误信息:检查错误是否会泄露敏感信息
自动化安全测试
在测试套件中加入安全测试:
describe("Security Tests", () => {
it("should not allow access without authentication", async () => {
const response = await request.get("/api/user/rules/123");
expect(response.status).toBe(401);
});
it("should not allow access to other users' resources", async () => {
const response = await request
.get("/api/user/rules/other-user-rule-id")
.set("Authorization", "Bearer valid-token")
.set("X-Email-Account-ID", "user-account-id");
expect(response.status).toBe(404); // Not 403, to avoid info disclosure
});
});
部署安全
环境变量
- 所有敏感数据均存储在环境变量中
- 代码或版本控制中不包含任何密钥
- 不同环境使用不同的密钥
监控与日志记录
- 记录安全事件
- 跟踪身份验证失败尝试
- 监控异常访问模式
- 日志中不包含敏感数据
请记住:安全不容妥协。每个处理用户数据的 API 路由都必须遵循这些指南。如有疑问,请采取谨慎做法并添加额外的安全检查。
评论
支持 Markdown