关于 convex-security-audit
面向 Convex 应用后端的全面审计技能,供发布前或安全事件后进行检查。它梳理公开函数、数据访问、路由、存储、计划任务和密钥,并按严重程度整理发现及修复建议。
主要功能
- 01盘点公开函数
- 02梳理身份验证与表访问
- 03检查 HTTP 路由和存储
- 04追踪计划任务与内部调用
- 05检查环境密钥
- 06生成分级问题报告
适用场景
- 01上线前审计后端
- 02调查可疑的数据访问
- 03检查多租户或计费功能
- 04编写安全审计报告
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-security-audit安装后在 Claude Code 中输入/convex-security-audit确认已加载。
详细介绍
Convex 安全审计
生成一份书面发现报告,涵盖每个公共函数、每张表和每个入口点(HTTP 路由、文件存储、调度器)。唯一的规则是:每个导出的 query、mutation 和 action 都是任何人都能在互联网上使用任意参数调用的公共端点,因此每个端点都必须证明调用者的身份以及其可访问的内容。
如果只进行十分钟的检查,请改用 convex-security-check。此技能是耗时更长、覆盖更全面的版本。
适用场景
- 正式上线或重大版本发布之前
- 发生安全事件、密钥泄露或可疑数据访问之后
- 代码库中的公共函数已远超几个,却无人梳理谁能调用什么时
- 添加多租户、管理员或计费功能时
- 用户要求“全面审计”“安全审查”或“检查整个后端”时
参考文件
- references/authorization-patterns.md:修复发现的问题时打开。文件中包含
getCurrentUser助手、用于经过身份验证、管理员和租户范围访问的customQuery和customMutation包装器、通过索引进行的所有权检查以及角色检查。 - references/attack-surface.md:在第 4 到第 7 步期间打开。文件涵盖 HTTP 操作、存储 URL 泄露、调度器信任、使用
@convex-dev/rate-limiter实现速率限制、环境变量中的密钥,以及客户端提供的 ID。 - references/audit-report-template.md:在第 8 步打开。这是发现报告模板,以及要填写的每表数据访问矩阵。
审计流程
按顺序完成以下步骤。边审计边记录每项发现。清单完成之前不要修复任何问题;过早修复会掩盖问题的整体情况。
1. 清点公共函数
列出每个导出的 query、mutation 和 action。这些都是攻击面。
rg -n "export const \w+ = (query|mutation|action)\(" convex --glob '!**/_generated/**'
对每个函数记录:文件、名称、类型、客户端是否需要调用,以及该函数是否成本高昂(例如发送邮件、调用大模型、创建记录),因而需要速率限制。任何仅由其他 Convex 函数、cron 或调度器调用的函数都应设为 internalQuery、internalMutation 或 internalAction。将每个此类情况标记为一项发现。
2. 按函数梳理身份验证
对于第 1 步中的每个公共函数,请回答:它是否会在读取或写入之前调用 ctx.auth.getUserIdentity()(或调用会执行此操作的助手)?哪些函数是有意允许匿名访问的?
rg -l "= (query|mutation|action)\(" convex --glob '!**/_generated/**' \
| xargs rg -L "getUserIdentity|getCurrentUser|authedQuery|authedMutation"
此处列出的文件都包含未在文件中的任何位置调用身份验证的公共函数。逐个阅读这些文件。公共博客列表允许匿名访问。但如果这类函数会返回或写入用户数据,则属于严重问题。
检查身份验证助手在缺少身份信息时是否会抛出错误或提前返回,并确认角色检查读取的是 users 表中的角色,而不是客户端参数中的角色。
3. 按表梳理数据访问
对于 convex/schema.ts 中的每张表,列出哪些公共函数会读取或写入该表,以及限制访问范围的条件是什么(所有者 ID、组织成员身份、角色、公开标志)。填写报告模板中的数据访问矩阵。
注意以下情况:
- 未限制范围的读取:在公共函数中调用
ctx.db.query("table").collect() - 调用
ctx.db.get(args.someId)后直接返回或执行 patch,却未比较所有权 - 将所有权与可伪造的值(电子邮件、显示名称)比较,而不是与
user._id比较 - 使用
.filter()而非.withIndex()限制范围;这通常说明范围限制是事后才考虑的 - 返回验证器泄露字段:使用
v.any(),或展开包含passwordHash、stripeCustomerId或内部标志的完整文档
rg -n "v\.any\(\)" convex --glob '!**/_generated/**'
rg -n "ctx\.db\.(get|patch|delete|replace)\(args\." convex --glob '!**/_generated/**'
4. 审查 http.ts
convex/http.ts 中的每个路由都可以在没有 Convex 客户端的情况下访问。针对每个路由,检查:如何验证调用方(Webhook 签名、Bearer 令牌、ctx.auth.getUserIdentity())?使用请求正文前是否先对其进行验证?是否只调用携带已验证数据的 internal.* 函数?
rg -n "http\.route|httpAction\(" convex/http.ts
跳过签名验证的 Webhook 处理程序属于严重问题。在 httpAction 内调用 api.* 函数的路由,可能绕过这些函数所依赖的身份验证。
5. 审查文件存储
查找生成上传 URL 的位置,以及返回文件 URL 的位置。
rg -n "storage\.(generateUploadUrl|getUrl|delete)" convex --glob '!**/_generated/**'
如果公开 mutation 中的 generateUploadUrl 没有身份验证,任何人都能占满你的存储空间。查询若对客户端传入的任意 ID 返回 ctx.storage.getUrl(args.fileId),就会造成数据泄露;任何持有该 URL 的人都能使用它。
6. 审查计划任务和内部边界
内部函数按设计会跳过身份验证。需要确认的是,是否有任何代码使用未经验证的输入来调度这些函数。
rg -n "(runAfter|runAt|runMutation|runAction|interval|cron)\([^)]*\bapi\." convex --glob '!**/_generated/**'
调度 api.* 函数属于问题:应改用 internal.*。然后沿着每个 internal.* 调用点追溯到触发它的公开函数或路由,并确认调用方在调度前验证了数据归属。
7. 审查环境变量和密钥
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\." convex src --glob '!**/_generated/**'
密钥应存储在 Convex 环境变量中(npx convex env set NAME value),并通过发起外部调用的 action 或 HTTP action 中的 process.env 读取。源代码中出现密钥字面量属于严重问题。客户端代码(src/)中从 process.env 读取的任何内容都是公开的;只有非敏感的 VITE_ 或 NEXT_PUBLIC_ 值才应放在那里。确认开发和生产部署使用不同的密钥。
8. 编写报告
填写 references/audit-report-template.md。每个问题单独列为一项发现,按严重程度排序,并注明位置、证据和具体修复方案。包含数据访问矩阵。最后列出已审查并确认可以匿名访问的公开函数,避免下一位审计人员重复工作。
严重程度等级
| 严重程度 | 含义 | 示例 |
|---|---|---|
| 严重 | 任何匿名调用方都能读取或更改其他用户的数据,或密钥已泄露 | 公开 mutation 在未检查数据归属的情况下,根据客户端提供的 ID 修改文档;源代码中包含 API 密钥;Webhook 未检查签名 |
| 高 | 已通过身份验证的用户能访问其权限范围之外的数据或操作 | 多租户查询缺少组织成员身份检查;管理函数从客户端参数中读取角色 |
| 中 | 防御纵深存在缺口,但目前没有直接利用方式 | 应设为内部函数的公开函数;高成本操作没有速率限制;上传 URL 未进行身份验证 |
| 低 | 代码卫生问题 | 对非敏感参数使用 v.any();缺少返回值验证器;存在索引时仍使用 .filter() |
发现示例
Severity: Critical
Location: convex/tasks.ts, updateTask (mutation, public)
Evidence: handler calls ctx.db.patch(args.taskId, { title: args.title }) after
requireAuth(ctx) but never compares task.userId to the caller. Any signed in
user can rename any task by guessing or capturing an ID.
Fix: load the task, compare task.userId to user._id, throw on mismatch. See
references/authorization-patterns.md, "Ownership checks".
Verified: reproduced by calling updateTask with another user's task ID from the
dashboard function runner.
常见错误
| 错误 | 造成的问题 | 正确做法 |
|---|---|---|
不追溯调用方,就认为 internalMutation 是安全的 | 调度它的公开函数可能传入未经验证的 ID | 审计调度调用点,而不只是内部函数 |
| 只在 UI 中检查身份验证 | 任何人都能通过仪表板或脚本调用该函数 | 每个公开处理程序都自行检查身份 |
将数据归属与 identity.email 比较 | 不同提供商可能重复使用或未验证电子邮件地址 | 通过 tokenIdentifier 查找用户,并与 user._id 比较 |
对同一种情况混用 return null 和 throw | 响应差异会泄露记录是否存在 | 在查询中,对“未找到”和“非本人数据”都返回 null |
| 只在客户端进行速率限制 | 攻击者会绕过客户端 | 在 mutation 或 action 内使用 @convex-dev/rate-limiter |
| 修复后未重新运行 grep 就签字确认 | 修复经常会转移问题 | 修复后重新执行步骤 1 到 7 |
检查清单
- 列出每个导出的
query、mutation、action及其预期调用方 - 每个应为内部函数的函数都使用
internal* - 每个公共函数都会检查身份,或注明其有意允许匿名调用
- 数据访问矩阵中已填写每张表对应的行
- 每个
http.ts路由都会验证调用方并校验请求体 - 上传 URL 的生成和文件 URL 的读取均受所有权校验保护
- 调度器、cron 或
ctx.run*调用中不包含api.*引用 - 源码中没有机密字面值;开发环境和生产环境使用不同的密钥
- 对高成本或易被滥用的 mutation 和 action 进行速率限制
- 报告针对每个发现列出严重程度、位置、证据和修复方案