关于 convex-security-check
面向 Convex 项目的快速安全检查技能,用于审查 `convex/` 中的公开函数、身份验证、参数验证、数据所有权和密钥处理。它通过检查清单和 grep 命令定位常见问题,不代替完整审计。
主要功能
- 01检查公开函数与内部函数的区分
- 02检查身份验证和数据所有权
- 03检查参数及返回值验证
- 04扫描密钥和 HTTP 路由风险
- 05提供升级到完整审计的条件
适用场景
- 01合并涉及 `convex/` 的变更前检查
- 02新增或重命名公开函数后排查风险
- 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 暴露、存储、调度器信任和速率限制,并生成书面问题报告。本检查不会执行这些工作。