Convex安全审计清单Skill convex-security-check

这个技能提供了Convex应用程序的快速安全审计清单,覆盖认证、函数暴露、参数验证、行级访问控制和环境变量处理,帮助开发者检查和优化应用安全性。适用于安全审计、代码审查和漏洞预防,关键词:Convex安全、审计清单、认证授权、参数验证、访问控制、环境变量管理、函数安全。

安全审计 0 次安装 0 次浏览 更新于 3/17/2026

名称: convex-security-check 显示名称: Convex 安全审计清单 描述: 快速安全审计清单,覆盖认证、函数暴露、参数验证、行级访问控制和环境变量处理 版本: 1.0.0 作者: Convex 标签: [convex, 安全, 认证, 授权, 清单]

Convex 安全审计清单

一个用于Convex应用程序的快速安全审计清单,覆盖认证、函数暴露、参数验证、行级访问控制和环境变量处理。

文档来源

在实施前,不要假设;获取最新文档:

说明

安全检查清单

使用此清单快速审计您的Convex应用程序安全:

1. 认证

  • [ ] 认证提供者配置(Clerk、Auth0等)
  • [ ] 所有敏感查询检查 ctx.auth.getUserIdentity()
  • [ ] 未认证访问在有意处明确允许
  • [ ] 会话令牌正确验证

2. 函数暴露

  • [ ] 公共函数(querymutationaction)已审核
  • [ ] 内部函数使用 internalQueryinternalMutationinternalAction
  • [ ] 无敏感操作作为公共函数暴露
  • [ ] HTTP动作验证来源/认证

3. 参数验证

  • [ ] 所有函数有明确的 args 验证器
  • [ ] 所有函数有明确的 returns 验证器
  • [ ] 未使用 v.any() 处理敏感数据
  • [ ] ID验证器使用正确表名

4. 行级访问控制

  • [ ] 用户只能访问自己的数据
  • [ ] 管理函数检查用户角色
  • [ ] 共享资源有适当访问检查
  • [ ] 删除函数验证所有权

5. 环境变量

  • [ ] API密钥存储在环境变量中
  • [ ] 代码或模式中无秘密
  • [ ] 开发/生产环境使用不同密钥
  • [ ] 环境变量仅在动作中访问

认证检查

// convex/auth.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";

// 辅助函数要求认证
async function requireAuth(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new ConvexError("需要认证");
  }
  return identity;
}

// 安全查询模式
export const getMyProfile = query({
  args: {},
  returns: v.union(v.object({
    _id: v.id("users"),
    name: v.string(),
    email: v.string(),
  }), v.null()),
  handler: async (ctx) => {
    const identity = await requireAuth(ctx);
    
    return await ctx.db
      .query("users")
      .withIndex("by_tokenIdentifier", (q) => 
        q.eq("tokenIdentifier", identity.tokenIdentifier)
      )
      .unique();
  },
});

函数暴露检查

// 公共 - 向客户端暴露(仔细审核!)
export const listPublicPosts = query({
  args: {},
  returns: v.array(v.object({ /* ... */ })),
  handler: async (ctx) => {
    // 任何人都可调用 - 有意公开
    return await ctx.db
      .query("posts")
      .withIndex("by_public", (q) => q.eq("isPublic", true))
      .collect();
  },
});

// 内部 - 仅可从其他Convex函数调用
export const _updateUserCredits = internalMutation({
  args: { userId: v.id("users"), amount: v.number() },
  returns: v.null(),
  handler: async (ctx, args) => {
    // 这不能直接从客户端调用
    await ctx.db.patch(args.userId, {
      credits: args.amount,
    });
    return null;
  },
});

参数验证检查

// 好:严格验证
export const createPost = mutation({
  args: {
    title: v.string(),
    content: v.string(),
    category: v.union(
      v.literal("tech"),
      v.literal("news"),
      v.literal("other")
    ),
  },
  returns: v.id("posts"),
  handler: async (ctx, args) => {
    const identity = await requireAuth(ctx);
    return await ctx.db.insert("posts", {
      ...args,
      authorId: identity.tokenIdentifier,
    });
  },
});

// 坏:弱验证
export const createPostUnsafe = mutation({
  args: {
    data: v.any(), // 危险:允许任何数据
  },
  returns: v.id("posts"),
  handler: async (ctx, args) => {
    return await ctx.db.insert("posts", args.data);
  },
});

行级访问控制检查

// 更新前验证所有权
export const updateTask = mutation({
  args: {
    taskId: v.id("tasks"),
    title: v.string(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await requireAuth(ctx);
    
    const task = await ctx.db.get(args.taskId);
    
    // 检查所有权
    if (!task || task.userId !== identity.tokenIdentifier) {
      throw new ConvexError("无权更新此任务");
    }
    
    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});

// 删除前验证所有权
export const deleteTask = mutation({
  args: { taskId: v.id("tasks") },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await requireAuth(ctx);
    
    const task = await ctx.db.get(args.taskId);
    
    if (!task || task.userId !== identity.tokenIdentifier) {
      throw new ConvexError("无权删除此任务");
    }
    
    await ctx.db.delete(args.taskId);
    return null;
  },
});

环境变量检查

// convex/actions.ts
"use node";

import { action } from "./_generated/server";
import { v } from "convex/values";

export const sendEmail = action({
  args: {
    to: v.string(),
    subject: v.string(),
    body: v.string(),
  },
  returns: v.object({ success: v.boolean() }),
  handler: async (ctx, args) => {
    // 从环境访问API密钥
    const apiKey = process.env.RESEND_API_KEY;
    
    if (!apiKey) {
      throw new Error("RESEND_API_KEY未配置");
    }
    
    const response = await fetch("https://api.resend.com/emails", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: "noreply@example.com",
        to: args.to,
        subject: args.subject,
        html: args.body,
      }),
    });
    
    return { success: response.ok };
  },
});

示例

完整安全模式

// convex/secure.ts
import { query, mutation, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { ConvexError } from "convex/values";

// 认证辅助函数
async function getAuthenticatedUser(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    throw new ConvexError({
      code: "UNAUTHENTICATED",
      message: "您必须登录",
    });
  }
  
  const user = await ctx.db
    .query("users")
    .withIndex("by_tokenIdentifier", (q) => 
      q.eq("tokenIdentifier", identity.tokenIdentifier)
    )
    .unique();
    
  if (!user) {
    throw new ConvexError({
      code: "USER_NOT_FOUND",
      message: "用户资料未找到",
    });
  }
  
  return user;
}

// 检查管理员角色
async function requireAdmin(ctx: QueryCtx | MutationCtx) {
  const user = await getAuthenticatedUser(ctx);
  
  if (user.role !== "admin") {
    throw new ConvexError({
      code: "FORBIDDEN",
      message: "需要管理员访问权限",
    });
  }
  
  return user;
}

// 公共:列出自己的任务
export const listMyTasks = query({
  args: {},
  returns: v.array(v.object({
    _id: v.id("tasks"),
    title: v.string(),
    completed: v.boolean(),
  })),
  handler: async (ctx) => {
    const user = await getAuthenticatedUser(ctx);
    
    return await ctx.db
      .query("tasks")
      .withIndex("by_user", (q) => q.eq("userId", user._id))
      .collect();
  },
});

// 仅管理员:列出所有用户
export const listAllUsers = query({
  args: {},
  returns: v.array(v.object({
    _id: v.id("users"),
    name: v.string(),
    role: v.string(),
  })),
  handler: async (ctx) => {
    await requireAdmin(ctx);
    
    return await ctx.db.query("users").collect();
  },
});

// 内部:设置用户角色(永不暴露)
export const _setUserRole = internalMutation({
  args: {
    userId: v.id("users"),
    role: v.union(v.literal("user"), v.literal("admin")),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.patch(args.userId, { role: args.role });
    return null;
  },
});

最佳实践

  • 除非明确指示,否则不要运行 npx convex deploy
  • 除非明确指示,否则不要运行任何git命令
  • 在返回敏感数据前始终验证用户身份
  • 对敏感操作使用内部函数
  • 用严格验证器验证所有参数
  • 在更新/删除操作前检查所有权
  • 将API密钥存储在环境变量中
  • 审核所有公共函数的安全影响

常见陷阱

  1. 缺少认证检查 - 始终验证身份
  2. 暴露内部操作 - 使用internalMutation/Query
  3. 信任客户端提供的ID - 验证所有权
  4. 使用v.any()处理参数 - 使用特定验证器
  5. 硬编码秘密 - 使用环境变量

参考