mcpField
C
convex-security-check

convex-security-check

github.com/waynesutton安全4052.7k 次安装4 周前更新
代码仓库

关于 convex-security-check

面向 Convex 项目的快速安全检查技能,用于审查 `convex/` 中的公开函数、身份验证、参数验证、数据所有权和密钥处理。它通过检查清单和 grep 命令定位常见问题,不代替完整审计。

主要功能

  1. 01检查公开函数与内部函数的区分
  2. 02检查身份验证和数据所有权
  3. 03检查参数及返回值验证
  4. 04扫描密钥和 HTTP 路由风险
  5. 05提供升级到完整审计的条件

适用场景

  1. 01合并涉及 `convex/` 的变更前检查
  2. 02新增或重命名公开函数后排查风险
  3. 03部署到生产环境前进行快速检查

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/waynesutton/builder-skills --skill convex-security-check

安装后在 Claude Code 中输入/convex-security-check确认已加载。

附带文件agents/openai.yamlassets/small-logo.svg

详细介绍

Convex 安全检查

花 10 分钟检查 convex/,找出最常见的上线错误。规则只有一条:每个导出的 query、mutation 和 action 都是任何人都能用任意参数调用的公共端点,因此每个端点都必须检查身份和所有权,或明确允许匿名访问。

逐个运行 grep,查看结果,然后修复或记录问题。此技能用于发现问题,不会梳理整个系统。要进行全面检查,请交给 convex-security-audit。

何时使用

  • 合并涉及 convex/ 的拉取请求前
  • 添加或重命名公共函数后
  • 用户说“快速安全检查”“检查一下后端是否正常”或“这里有没有明显的问题”
  • 从开发环境推送到生产环境前,但没有时间进行全面审计时

检查清单

在项目根目录运行这些命令。每条 grep 命令都会排除 _generated。

1. 公共函数与内部函数

只由其他 Convex 函数、调度器、定时任务或 Webhook 调用的函数应使用 internalQuery、internalMutation 或 internalAction。

rg -n "export const \w+ = (query|mutation|action)\(" convex --glob '!**/_generated/**'
rg -n "\bapi\.\w+\.\w+" convex --glob '!**/_generated/**'

第二条 grep 会查找后端内部使用的 api.*。服务端代码调度或运行公共函数,几乎总是表明该函数应该设为内部函数。

  • 每个公共函数都有需要调用它的客户端
  • ctx.scheduler.*、ctx.run* 或 crons.* 中没有 api.*
  • 仅限管理员的操作(更改角色、发放额度、跨用户删除)都是内部函数
2. 身份验证
rg -l "= (query|mutation|action)\(" convex --glob '!**/_generated/**' \
  | xargs rg -L "getUserIdentity|getCurrentUser|authedQuery|authedMutation"

列出的文件包含公共函数,但完全没有身份验证调用。逐个打开检查。公开帖子列表可以允许匿名访问。任何返回或写入用户数据的函数都不行。

  • 每个公共函数都会检查身份,或注明允许匿名访问
  • 身份验证辅助函数会在身份缺失时抛出错误或提前返回
  • 角色来自 users 表,而不是客户端参数
3. 验证
rg -nU "(query|mutation|action)\(\{\s*handler" convex --glob '!**/_generated/**'
rg -n "v\.any\(\)" convex --glob '!**/_generated/**'

第一条 grep 会查找定义以 handler 开头的函数,这意味着没有 args。第二条会查找 v.any(),它会关闭相应字段的验证。

  • 每个函数都有 args 和 returns 验证器
  • 不对传入数据库或外部 API 的参数使用 v.any()
  • 返回验证器明确列出字段,确保 passwordHash、stripeCustomerId 和内部标志不会泄露
4. 所有权与 ID

v.id("tasks") 可以证明字符串是该表的有效 ID,但不能证明调用者拥有该文档。

rg -n "ctx\.db\.(get|patch|delete|replace)\(args\." convex --glob '!**/_generated/**'
rg -n "userId: v\.(id|string)\(" convex --glob '!**/_generated/**'

对于第一条 grep 中的每个匹配项,检查读取和写入之间是否有所有权或成员资格比较。对于第二条,接受调用者自己的 userId 作为参数的公共函数,等于相信客户端提供的身份信息。

  • 对客户端提供的 ID 执行 get、patch、delete、replace 后,都会进行所有权或成员资格检查
  • 调用者身份来自 ctx.auth,绝不作为参数接收
  • 列表查询使用所有者字段上的 withIndex,而不是全表扫描后再筛选
5. 密钥
rg -n -i "(sk_live|sk_test|whsec_|AKIA[0-9A-Z]{16}|-----BEGIN|api[_-]?key\s*[:=]\s*['\"][A-Za-z0-9])" convex src --glob '!**/_generated/**'
rg -n "process\.env\." src

src/ 中任何读取 process.env 或 import.meta.env 的代码都会随应用发布到浏览器端。其中只能包含部署 URL 和公开客户端 ID。

  • convex/、src/、测试或 fixture 中没有硬编码的密钥
  • 在使用密钥的 action 或 HTTP action 内读取 process.env 中的密钥
  • 开发和生产部署使用不同的密钥
6. HTTP 与存储
rg -n "http\.route|httpAction\(" convex/http.ts
rg -n "storage\.(generateUploadUrl|getUrl)" convex --glob '!**/_generated/**'
  • 每个 http.ts 路由都会在解析请求正文前验证调用者(Webhook 签名、Bearer 令牌或 getUserIdentity)
  • 路由调用 internal.*,而不是 api.*
  • generateUploadUrl 需要身份验证
  • getUrl 使用的存储 ID 来自调用者拥有的文档,而不是客户端传入的存储 ID

一处修复,前后对比

最常见的问题:检查了身份验证,却没有检查所有权。

修复前:

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("Sign in required");
    // any signed in user can rename any task
    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});

修复后:

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

export const updateTask = mutation({
  args: { taskId: v.id("tasks"), title: v.string() },
  returns: v.null(),
  handler: async (ctx, args) => {
    const user = await getCurrentUser(ctx); // throws when signed out
    const task = await ctx.db.get(args.taskId);
    // same error for missing and not owned, so IDs cannot be probed
    if (!task || task.userId !== user._id) {
      throw new ConvexError({ code: "NOT_FOUND", message: "Task not found" });
    }
    await ctx.db.patch(args.taskId, { title: args.title });
    return null;
  },
});

getCurrentUser 通过 by_tokenIdentifier 索引,使用 identity.tokenIdentifier 查找用户;如果没有身份信息,则会抛出错误。在 convex/lib/auth.ts 中定义一次,并在所有地方使用。

常见错误

错误造成的问题应该这样做
只在 React 组件中检查身份验证任何人都可以从仪表板或脚本调用该函数在处理函数中检查
使用 v.id("users") 参数表示“当前用户”客户端可以传入任意用户的 ID从 ctx.auth.getUserIdentity() 获取
将所有权与 identity.email 比较邮箱可能被重复使用,或未经验证与 user._id 比较
认为 internalMutation 本身就是安全的公共调用方可能会传入未经验证的 ID同时检查调用位置
修复一处后就继续同类问题通常也会出现在相邻代码中修复 grep 查找出的所有问题

移交给 convex-security-audit

出现以下任一情况时,停止并运行完整审计:

  • 步骤 2 或 4 中发现的问题超过两三个;这表明问题具有普遍性
  • 应用包含多租户数据(组织、团队、工作区)
  • http.ts 中有 Webhook 路由,或返回用户数据的路由
  • 文件会被上传和提供
  • 用户要求提供报告、发布前审查或事件后审查

该审计会按函数梳理身份验证、按表梳理数据访问、检查 HTTP 暴露、存储、调度器信任和速率限制,并生成书面问题报告。本检查不会执行这些工作。

文档

评论

支持 Markdown