mcpField
S
security

security

github.com/elie222安全12.8k36 次安装今天更新
代码仓库

关于 security

用于审查 Inbox Zero 代码的安全指南技能,涵盖路由认证、按用户限定的数据访问、输入校验、安全错误处理和定时任务保护。

主要功能

  1. 01选择认证中间件
  2. 02按用户和账户限定数据
  3. 03校验资源所有权
  4. 04输入校验与清理
  5. 05安全错误处理
  6. 06定时任务端点认证

适用场景

  1. 01审查 API 路由的认证方式
  2. 02检查数据库查询是否限定用户数据
  3. 03测试资源所有权防护以发现 IDOR
  4. 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 },
});
安全审查要点

审查代码时,请特别关注:

  1. 新增 API 路由 - 确保正确使用中间件
  2. 数据库查询 - 验证用户范围限定
  3. 参数处理 - 检查验证和清理
  4. 错误响应 - 确保不会泄露信息
  5. 批量操作 - 对批量更新/删除格外谨慎

安全测试

手动测试清单
  1. 绕过身份验证:尝试在不提供身份验证标头的情况下访问端点
  2. IDOR 测试:修改资源 ID,尝试访问其他用户的数据
  3. 参数篡改:使用无效/恶意参数进行测试
  4. 错误信息:检查错误是否会泄露敏感信息
自动化安全测试

在测试套件中加入安全测试:

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