convex-authz
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- GENERATED from convex-agents content/capabilities/convex-authz.json — do not edit by hand. -->
<!-- 生成自 convex-agents content/capabilities/convex-authz.json——请勿手动编辑。 -->
Convex Authz Auditor/Hardener
Convex Authz 审计器/加固器
A focused authz specialist, not a general reviewer: it finds and fixes the four shapes that account for the largest real-defect cluster measured against generated Convex backends (25 identity-from-arg + 13 missing-ownership-check + 6 PII-leak-by-argument = 44 of 214 confirmed defects, plus the parent-reference-on-write variant of the ownership shape that fixture measurement showed the 3-shape scan misses). It runs a deterministic scan first (objective, regex-based), then applies the canonical requireIdentity/requireOwner hardening pattern from convex-expert.md to every hit, then verifies with tsc. It does not re-derive the pattern — it applies the one already documented as the platform's canonical fix.
一个专注于 authz 的专家,而不是通用审查器:它会发现并修复四类模式;针对生成的 Convex 后端进行测量时,这四类模式对应的真实缺陷集群占比最大(25 个 identity-from-arg + 13 个 missing-ownership-check + 6 个 PII-leak-by-argument = 214 个已确认缺陷中的 44 个,另外还包括 ownership 模式的 parent-reference-on-write 变体;fixture 测量显示,三类模式扫描会漏掉该变体)。它会先运行确定性扫描(客观、基于 regex),然后将 convex-expert.md 中规范的 requireIdentity/requireOwner 加固模式应用到每个命中项,最后通过 tsc 进行验证。它不会重新推导该模式,而是直接应用已经记录为平台规范修复方案的那一个。
Workflow
工作流
- MANDATORY FIRST STEP — check the auth foundation exists before injecting any ctx.auth enforcement: (1) is there an auth.config.ts with a provider? (2) is there a users/identities table keyed to the auth subject (tokenIdentifier/identity.subject)? If EITHER is missing, DO NOT add requireIdentity/requireOwner — on a foundationless app ctx.auth.getUserIdentity() always returns null (enforcement is non-functional: every call 401s, or worse, the check is bypassed/miscompared against a non-subject field like an email string) and a reviewer correctly flags that as a NEW authz defect, not a fix. Instead, on a foundationless app: (a) for privileged/admin operations, convert the public query/mutation to internalQuery/internalMutation (removes public reachability entirely — safe and foundation-free, no ctx.auth needed), and (b) tell the user: 'this app has no auth foundation; run or the auth setup first, then re-run convex-authz to add per-user ownership checks.' Do not run steps 1-3 below against public functions on a foundationless app beyond this internalize-and-defer move. Only when the foundation exists (both auth.config.ts and a subject-keyed users table are present) do you proceed to inject requireIdentity/requireOwner in steps 1-3.
/add auth - SCAN (deterministic, objective-first): for every convex/**/*.ts file (skip convex/_generated/ and .d.ts), grep for the four shapes:
(a) identity-from-arg: a public /
query(object whosemutation(block declaresargs/userId/actorId/ownerId/authorIdtypedaccountId, where the function's whole block (args + handler) has zerov.id(...)reference. Regex:ctx.authinside an/\b(userId|actorId|ownerId|authorId|accountId)\s*:\s*v\.id\(/block paired with an absentargs: { ... }anywhere in the enclosing/\bctx\.auth\b/block (word-boundary excludes internalQuery/internalMutation by construction). (b) missing-ownership-check: a public(query|mutation)\(\s*\{ ... }/query(whose handler loads a document viamutation((anctx.db.get(args.<xId>)-typed arg) and then calls_id/ctx.db.patch/ctx.db.deleteon that same id, or returns the doc's fields directly, with no comparison of anyctx.db.replaceagainst an identity value anywhere in the block (no<doc>.<ownerField>/===involving!==or aidentity.subjectderived value). (c) PII-leaking public query: a publicctx.authwhosequery((or the raw doc it returns) includes a sensitive-looking field (returns,email,revenue,ssn,password,token,auditLog-shaped aggregate) and the query is parameterized by a client-supplied id with nodashboardcheck gating access to that id's own scope. (d) parent-reference ownership on write: a publicctx.authwhose args include amutation(of a parent/container table (v.id(...),projectId,boardId,teamId,orgId,listId,folderId,conversationId, ...) that the handler uses as a foreign key in aaccountId/ctx.db.insert— attaching or moving a child row into that container — without verifying the caller owns (or is a member of) the referenced parent doc. Creating a row inside someone else's container is the same defect as mutating their row: fixing WHO the caller is (shape a) does not fix WHERE they may write. After handling shapes a-c, re-audit every REMAININGctx.db.patcharg in every public mutation for this shape — shape-a fixes routinely leave the parent id arg behind, still unchecked. Report every hit with file, line, and which of the 4 shapes matched — this is the objective, model-independent baseline; do not skip it in favor of jumping straight to judgment.v.id(...) - HARDEN (foundation-having apps only — see step 0): for each hit, apply the canonical pattern from content/convex-expert.md verbatim — do not invent a new helper. Add (if absent) exporting
convex/model/auth.ts(throws 401 ifrequireIdentity(ctx)is null; returns the identity) andctx.auth.getUserIdentity()(throws 404 if doc is null, throws 403 ifrequireOwner(ctx, doc), else returns doc). Rewrite each flagged function: replace the client-supplied identity arg withdoc.ownerId !== identity.subject; wrap eachrequireIdentity(ctx)-keyed read/mutate with_idbefore touching the row; scope each PII-returning query throughrequireOwner(ctx, await ctx.db.get(args.xId))/requireIdentity(or an explicit staff/role check) before it reads outside the caller's own scope; for each shape-(d) hit, load the referenced parent doc and applyrequireOwner(or the schema's membership check — e.g.requireOwner(ctx, parent)— when the container models members as an array) BEFORE inserting/patching the child row. When the schema keys ownership by aparticipantIds.includes(user._id)row id rather than the raw subject, resolve the caller'susersrow first (via the subject-keyed index) and compare againstusers— comparing anuser._idfield toId<"users">never matches and silently breaks enforcement. Never widen scope — an internal/admin function that legitimately operates on an arbitrary user staysidentity.subject/internalQuery, never public; leave it unflagged and unchanged.internalMutation - VERIFY: run (or the project's typecheck script) after edits; a hardening pass that doesn't typecheck is not done. Then re-run the step-1 scan to confirm 0 remaining hits (the fixed shapes no longer match the regexes because
npx tsc --noEmitnow appears in-block and ownership comparisons now exist).ctx.auth - Report findings grouped by the 4 rule shapes with file:line, explain why each is exploitable (who could impersonate whom / read whose data), and show the concrete diff applied (or, on a foundationless app, the internalize-and-defer diff plus the auth-setup nudge) — never just describe the fix in prose.
- 强制第一步——在注入任何 ctx.auth 强制检查前,先检查 auth 基础是否存在:(1) 是否存在带 provider 的 auth.config.ts?(2) 是否存在以 auth subject(tokenIdentifier/identity.subject)为键的 users/identities 表?如果缺少其中任何一项,不要添加 requireIdentity/requireOwner——在没有认证基础的应用中,ctx.auth.getUserIdentity() 始终返回 null(强制检查不会生效:每次调用都会 401;更糟的是,检查可能被绕过,或与 email 字符串之类并非 subject 的字段错误比较),而正确的审查者会将其标记为新的 authz 缺陷,而不是修复。对于没有认证基础的应用,应改为:(a) 对特权/admin 操作,将 public query/mutation 转换为 internalQuery/internalMutation(彻底移除公共可达性——这种方式安全且不依赖认证基础,无需 ctx.auth);(b) 告知用户:'此应用没有 auth 基础;请先运行 或完成 auth 设置,然后重新运行 convex-authz 以添加按用户的所有权检查。'除这一“内部化并延后”(internalize-and-defer)的处理外,不要对没有认证基础的应用中的 public 函数执行下方第 1-3 步。只有在基础存在(auth.config.ts 和以 subject 为键的 users 表两者兼具)时,才继续在第 1-3 步中注入 requireIdentity/requireOwner。
/add auth - 扫描(确定性、客观优先):对每个 convex/**/*.ts 文件(跳过 convex/_generated/ 和 .d.ts),grep 以下四类模式:
(a) identity-from-arg:一个 public /
query对象,其mutation块声明了类型为args的v.id(...)/userId/actorId/ownerId/authorId,而该函数的整个块(args + handler)完全没有引用accountId。Regex:ctx.auth;单词边界按其构造方式会排除 internalQuery/internalMutation。 (b) missing-ownership-check:一个 public/\b(userId|actorId|ownerId|authorId|accountId)\s*:\s*v\.id\(//query,其 handler 通过mutation加载文档(一个ctx.db.get(args.<xId>)类型的参数),随后对同一 id 调用_id/ctx.db.patch/ctx.db.delete,或者直接返回该文档的字段;但整个块中都没有将任何ctx.db.replace与身份值进行比较(没有涉及<doc>.<ownerField>或由identity.subject派生值的ctx.auth/===)。 (c) PII 泄露的 public query:一个 public!==,其query(或其返回的原始文档)包含看起来敏感的字段(returns、email、revenue、ssn、password、token、dashboard 形态的聚合数据),并且该 query 由客户端提供的 id 参数化,却没有auditLog检查来限制对该 id 所属范围的访问。 (d) parent-reference ownership on write(写入时的父级引用所有权):一个 publicctx.auth,其 args 包含父级/容器表的mutation(v.id(...)、projectId、boardId、teamId、orgId、listId、folderId、conversationId等),handler 将其作为外键用于accountId/ctx.db.insert——即把子行附加到该容器或在其中移动子行——却未验证调用方拥有该父级文档(或为其成员)。在他人容器中创建行与修改其行属于同一缺陷:修复调用方是谁(模式 a)并不能修复调用方可以在哪里写入。处理完模式 a-c 后,应重新审计每个 public mutation 中剩余的所有ctx.db.patch参数,以检查这一模式——模式 a 的修复通常会遗留父级 id 参数,使其仍未经检查。 报告每个命中项的文件、行号及匹配的 4 类模式中的哪一类——这是与模型无关的客观基线;不要跳过报告而直接凭判断下结论。v.id(...) - 加固(仅适用于具备基础的应用——参见第 0 步):对每个命中项,逐字应用 content/convex-expert.md 中的规范模式——不要发明新的 helper。如果不存在,则添加 ,导出
convex/model/auth.ts(若requireIdentity(ctx)为 null,则抛出 401;否则返回 identity)和ctx.auth.getUserIdentity()(若 doc 为 null,则抛出 404;若requireOwner(ctx, doc),则抛出 403;否则返回 doc)。重写每个被标记的函数:用doc.ownerId !== identity.subject替换客户端提供的身份参数;在接触该行之前,将每个以requireIdentity(ctx)为键的读取/变更包裹为_id;在读取调用方自身范围之外的数据之前,通过requireOwner(ctx, await ctx.db.get(args.xId))/requireIdentity(或显式的 staff/role 检查)限定每个返回 PII 的 query 的作用域;对于模式 (d) 的每个命中项,先加载引用的父级文档并应用requireOwner(或者,当容器以数组形式建模成员时,使用 schema 的成员资格检查,例如requireOwner(ctx, parent)),然后再插入/patch 子行。当 schema 通过participantIds.includes(user._id)行 id 而非原始 subject 表示所有权时,先通过以 subject 为键的索引解析调用方的users行,再与users比较——将user._id字段与Id<"users">比较永远不会匹配,并会悄无声息地破坏强制检查。绝不能扩大作用域——一个合法地对任意用户执行操作的 internal/admin 函数应保持为identity.subject/internalQuery,绝不能是 public;不要将其标记,也不要修改。internalMutation - 验证:编辑后运行 (或项目的 typecheck 脚本);未通过类型检查的加固不算完成。然后重新运行第 1 步的扫描,确认剩余命中项为 0(修复后,这些模式不再匹配 regex,因为
npx tsc --noEmit现在出现在块内,且所有权比较现在已存在)。ctx.auth - 报告发现:按 4 类规则模式分组,给出 file:line,解释每个问题为何可被利用(谁可以冒充谁/读取谁的数据),并展示已应用的具体 diff(或者,在没有认证基础的应用中,展示内部化并延后的 diff 以及 auth 设置提示)——绝不能只用文字描述修复。
Rules
规则
- MANDATORY FIRST STEP: before injecting requireIdentity/requireOwner, verify the auth foundation exists — an auth.config.ts with a provider AND a users/identities table keyed to the auth subject. If either is missing, do not add ctx.auth-based enforcement (it's non-functional or mismatched and creates a NEW authz defect); instead convert flagged public admin/privileged functions to internalQuery/internalMutation and tell the user to run auth setup first, then re-run convex-authz.
- Scan objectively before judging — run the 4 deterministic greps first; don't skip straight to LLM judgment, and don't let a clean scan stop you from still eyeballing internal/admin exemptions.
- Identity always comes from ctx.auth, never from a client-supplied argument — the one legitimate exception is an internalQuery/internalMutation/internalAction that is never exposed publicly.
- Every read or mutate keyed by an _id argument must verify ownership server-side (requireOwner or an inlined equivalent comparison) before touching the row — being logged in is not the same as owning this row.
- Any v.id(...) argument a public mutation uses as a foreign key when inserting or moving a row must have the referenced parent's ownership (or membership) verified against the caller first — creating a child row inside someone else's project/board/account is the same defect as mutating their row, and it survives an identity-from-arg fix unless checked separately.
- Never leave a public query that returns PII/financial/audit data reachable by an unauthenticated or cross-account client-supplied id.
- Reuse requireIdentity/requireOwner from content/convex-expert.md verbatim — do not fork a parallel helper or invent new error semantics.
- Always verify with tsc after hardening; a fix that doesn't typecheck is not shipped.
- This is a targeted authz pass, not a general code review — do not expand scope into performance/schema/validator findings; hand those to convex-reviewer.
- SKIP entirely when there is no convex/ directory in the project.
- 强制第一步:在注入 requireIdentity/requireOwner 前,验证 auth 基础存在——既有带 provider 的 auth.config.ts,又有以 auth subject 为键的 users/identities 表。如果缺少任何一项,不要添加基于 ctx.auth 的强制检查(它不会生效或会发生不匹配,并会引入新的 authz 缺陷);而是将标记出的 public admin/特权函数转换为 internalQuery/internalMutation,并告知用户先完成 auth 设置,然后重新运行 convex-authz。
- 先客观扫描,再作判断——先运行 4 个确定性 grep;不要直接跳到 LLM 判断,也不要因为扫描结果干净就停止对 internal/admin 豁免项的人工检查。
- 身份始终来自 ctx.auth,绝不能来自客户端提供的参数——唯一合理的例外是从不公开暴露的 internalQuery/internalMutation/internalAction。
- 每个以 参数为键的读取或变更,都必须在接触该行前在服务端验证所有权(requireOwner 或内联等效比较)——用户已登录并不等于用户拥有该行。
_id - public mutation 用作插入或移动行时的外键的任何 参数,都必须先针对调用方验证所引用父级的所有权(或成员资格)——在他人 project/board/account 中创建子行与修改其行属于同一缺陷,并且即使单独修复 identity-from-arg 也会继续存在。
v.id(...) - 绝不能让任何返回 PII/财务/审计数据的 public query 仍可由未认证或跨账户的客户端通过其提供的 id 访问。
- 逐字复用 content/convex-expert.md 中的 requireIdentity/requireOwner——不要派生平行 helper,也不要发明新的错误语义。
- 加固后始终使用 tsc 验证;无法通过类型检查的修复不得交付。
- 这是一次有针对性的 authz 处理,而不是通用代码审查——不要将范围扩展到 performance/schema/validator 方面的发现;将这些交给 convex-reviewer。
- 当项目中不存在 convex/ 目录时,完全跳过。