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, mirrors the convex-backend-skill v1.7.9 lint advisory), 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后端中占比最高的实际缺陷(根据生成的Convex后端统计:25例参数伪造身份+13例缺失所有权校验+6例通过参数泄露PII=214个已确认缺陷中的44个,此外还能处理三类扫描未覆盖的写入时父引用所有权缺陷)。它首先执行确定性扫描(基于正则的客观扫描,与convex-backend-skill v1.7.9的lint建议一致),然后对每个命中项应用convex-expert.md中记录的标准requireIdentity/requireOwner加固模式,最后通过tsc进行验证。它不会重新推导修复模式,而是直接使用平台官方文档中已明确的标准修复方案。

Workflow

工作流程

  1. 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
    /add auth
    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.
  2. 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(
    /
    mutation(
    object whose
    args
    block declares
    userId
    /
    actorId
    /
    ownerId
    /
    authorId
    /
    accountId
    typed
    v.id(...)
    , where the function's whole block (args + handler) has zero
    ctx.auth
    reference. Regex:
    /\b(userId|actorId|ownerId|authorId|accountId)\s*:\s*v\.id\(/
    inside an
    args: { ... }
    block paired with an absent
    /\bctx\.auth\b/
    anywhere in the enclosing
    (query|mutation)\(\s*\{ ... }
    block (word-boundary excludes internalQuery/internalMutation by construction). (b) missing-ownership-check: a public
    query(
    /
    mutation(
    whose handler loads a document via
    ctx.db.get(args.<xId>)
    (an
    _id
    -typed arg) and then calls
    ctx.db.patch
    /
    ctx.db.delete
    /
    ctx.db.replace
    on that same id, or returns the doc's fields directly, with no comparison of any
    <doc>.<ownerField>
    against an identity value anywhere in the block (no
    ===
    /
    !==
    involving
    identity.subject
    or a
    ctx.auth
    derived value). (c) PII-leaking public query: a public
    query(
    whose
    returns
    (or the raw doc it returns) includes a sensitive-looking field (
    email
    ,
    revenue
    ,
    ssn
    ,
    password
    ,
    token
    ,
    auditLog
    ,
    dashboard
    -shaped aggregate) and the query is parameterized by a client-supplied id with no
    ctx.auth
    check gating access to that id's own scope. (d) parent-reference ownership on write: a public
    mutation(
    whose args include a
    v.id(...)
    of a parent/container table (
    projectId
    ,
    boardId
    ,
    teamId
    ,
    orgId
    ,
    listId
    ,
    folderId
    ,
    conversationId
    ,
    accountId
    , ...) that the handler uses as a foreign key in a
    ctx.db.insert
    /
    ctx.db.patch
    — 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 REMAINING
    v.id(...)
    arg 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.
  3. 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)
    convex/model/auth.ts
    exporting
    requireIdentity(ctx)
    (throws 401 if
    ctx.auth.getUserIdentity()
    is null; returns the identity) and
    requireOwner(ctx, doc)
    (throws 404 if doc is null, throws 403 if
    doc.ownerId !== identity.subject
    , else returns doc). Rewrite each flagged function: replace the client-supplied identity arg with
    requireIdentity(ctx)
    ; wrap each
    _id
    -keyed read/mutate with
    requireOwner(ctx, await ctx.db.get(args.xId))
    before touching the row; scope each PII-returning query through
    requireIdentity
    /
    requireOwner
    (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 apply
    requireOwner(ctx, parent)
    (or the schema's membership check — e.g.
    participantIds.includes(user._id)
    — when the container models members as an array) BEFORE inserting/patching the child row. When the schema keys ownership by a
    users
    row id rather than the raw subject, resolve the caller's
    users
    row first (via the subject-keyed index) and compare against
    user._id
    — comparing an
    Id<"users">
    field to
    identity.subject
    never matches and silently breaks enforcement. Never widen scope — an internal/admin function that legitimately operates on an arbitrary user stays
    internalQuery
    /
    internalMutation
    , never public; leave it unflagged and unchanged.
  4. VERIFY: run
    npx tsc --noEmit
    (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
    ctx.auth
    now appears in-block and ownership comparisons now exist).
  5. 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.
  1. 强制第一步 — 在注入任何ctx.auth校验前,先检查授权基础是否存在:(1) 是否有配置了认证提供者的auth.config.ts?(2) 是否有以认证主体(tokenIdentifier/identity.subject)为键的users/identities表?如果任意一项缺失,请勿添加requireIdentity/requireOwner — 在无授权基础的应用中,ctx.auth.getUserIdentity()始终返回null(校验完全失效:每次调用都会返回401,更糟的是校验会被绕过或错误地与非主体字段如邮箱字符串进行比较),评审者会将此标记为新的授权缺陷而非修复。对于无授权基础的应用,应采取以下措施:(a) 针对特权/管理员操作,将公开query/mutation转换为internalQuery/internalMutation(完全移除公开访问权限 — 无需授权基础即可安全运行,无需ctx.auth);(b) 告知用户:「此应用无授权基础,请先运行
    /add auth
    或完成授权设置,再重新运行convex-authz以添加按用户划分的所有权校验。」除了上述转换并延迟处理的操作外,请勿对无授权基础应用中的公开函数执行以下步骤1-3。只有当授权基础存在(同时具备auth.config.ts和以主体为键的users表)时,才能继续执行步骤1-3注入requireIdentity/requireOwner。
  2. 扫描(确定性、优先客观检测):遍历所有convex/**/*.ts文件(跳过convex/_generated/和.d.ts文件),匹配四类缺陷模式: (a) 参数伪造身份:公开的
    query(
    /
    mutation(
    对象,其
    args
    块声明了类型为
    v.id(...)
    userId
    /
    actorId
    /
    ownerId
    /
    authorId
    /
    accountId
    参数,且函数的整个代码块(参数+处理逻辑)中未引用
    ctx.auth
    。正则表达式:在
    args: { ... }
    块中匹配
    /\b(userId|actorId|ownerId|authorId|accountId)\s*:\s*v\.id\(/
    ,同时在包含它的
    (query|mutation)\(\s*\{ ... }
    块中未匹配到
    /\bctx\.auth\b/
    (单词边界限制确保不会匹配到internalQuery/internalMutation)。 (b) 缺失所有权校验:公开的
    query(
    /
    mutation(
    ,其处理逻辑通过
    ctx.db.get(args.<xId>)
    加载文档(
    <xId>
    _id
    类型参数),随后对同一id执行
    ctx.db.patch
    /
    ctx.db.delete
    /
    ctx.db.replace
    操作,或直接返回文档字段,但代码块中未将任何
    <doc>.<ownerField>
    与身份值进行比较(未出现涉及
    identity.subject
    ctx.auth
    派生值的
    ===
    /
    !==
    比较)。 (c) 泄露PII的公开查询:公开的
    query(
    ,其返回值(或直接返回的原始文档)包含敏感字段(如
    email
    revenue
    ssn
    password
    token
    auditLog
    、类
    dashboard
    的聚合数据),且该查询通过客户端提供的id进行参数化,但未通过
    ctx.auth
    校验限制对该id对应范围的访问。 (d) 写入时父引用所有权问题:公开的
    mutation(
    ,其参数包含父/容器表的
    v.id(...)
    类型值(如
    projectId
    boardId
    teamId
    orgId
    listId
    folderId
    conversationId
    accountId
    等),处理逻辑将其作为外键用于
    ctx.db.insert
    /
    ctx.db.patch
    操作 — 将子行附加或移动到该容器中,但未验证调用者是否拥有(或属于)引用的父文档。在他人的容器中创建子行与修改他人的行属于同一类缺陷:修复调用者身份(模式a)无法解决写入位置的问题。处理完模式a-c后,重新审计所有剩余公开mutation中的
    v.id(...)
    参数是否存在此类模式 — 模式a的修复通常会遗漏父id参数,导致其仍未被校验。 报告每个命中项的文件、行号以及匹配的四类模式之一 — 这是客观、独立于模型的基线,请勿跳过此步骤直接进行主观判断。
  3. 加固(仅适用于具备授权基础的应用 — 见步骤0):对每个命中项,严格按照content/convex-expert.md中的标准模式进行修复 — 请勿自行编写新的辅助函数。添加(若缺失)
    convex/model/auth.ts
    文件,导出
    requireIdentity(ctx)
    (若
    ctx.auth.getUserIdentity()
    为null则抛出401,否则返回身份信息)和
    requireOwner(ctx, doc)
    (若doc为null则抛出404,若
    doc.ownerId !== identity.subject
    则抛出403,否则返回doc)。重写每个标记的函数:将客户端提供的身份参数替换为
    requireIdentity(ctx)
    ;在操作行之前,用
    requireOwner(ctx, await ctx.db.get(args.xId))
    包裹每个以
    _id
    为键的读取/修改操作;通过
    requireIdentity
    /
    requireOwner
    (或明确的员工/角色校验)限制每个返回PII的查询范围,确保仅在调用者自身范围内读取;对于模式(d)的命中项,加载引用的父文档并在插入/修改子行之前应用
    requireOwner(ctx, parent)
    (或根据 schema 的成员校验逻辑 — 例如当容器将成员存储为数组时使用
    participantIds.includes(user._id)
    )。若schema通过
    users
    行id而非原始主体标识所有权,需先通过主体键索引解析调用者的
    users
    行,再与
    user._id
    进行比较 — 将
    Id<"users">
    字段与
    identity.subject
    比较永远不会匹配,会导致校验静默失效。请勿扩大范围 — 合法操作任意用户数据的内部/管理员函数应保持为
    internalQuery
    /
    internalMutation
    ,永远设为公开;保留其未标记状态且不做修改。
  4. 验证:编辑完成后运行
    npx tsc --noEmit
    (或项目的类型检查脚本);未通过类型检查的加固操作视为未完成。然后重新运行步骤1的扫描,确认无剩余命中项(修复后的模式不再匹配正则表达式,因为代码块中现在包含
    ctx.auth
    引用且存在所有权比较)。
  5. 报告结果:按四类规则模式分组展示命中项的file:line,解释每个缺陷的可利用性(谁可以冒充谁/读取谁的数据),并展示具体的修改差异(对于无授权基础的应用,展示转换为内部函数的差异以及授权设置提示) — 请勿仅用文字描述修复内容。

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.config.ts以及以认证主体为键的users/identities表。若任意一项缺失,请勿添加基于ctx.auth的校验(此类校验完全失效或匹配错误,会产生新的授权缺陷);应将标记的公开管理员/特权函数转换为internalQuery/internalMutation,并告知用户先完成授权设置,再重新运行convex-authz。
  • 先客观扫描再判断:先执行四次确定性正则扫描;请勿直接跳过扫描进行大语言模型主观判断,即使扫描结果干净,也需检查内部/管理员函数的豁免情况。
  • 身份始终来自ctx.auth,而非客户端提供的参数:唯一合法的例外是从未公开的internalQuery/internalMutation/internalAction。
  • 每个以_id参数为键的读取或修改操作,必须在服务器端验证所有权(使用requireOwner或等效的内联比较)后才能操作行 — 登录状态不等于拥有该行的权限。
  • 公开mutation用于插入或移动行时作为外键的任何v.id(...)参数,必须先验证调用者是否拥有(或属于)引用的父文档 — 在他人的项目/看板/账户中创建子行与修改他人的行属于同一类缺陷,若不单独校验,即使修复了参数伪造身份的问题,此类缺陷仍会存在。
  • 请勿保留可被未认证或跨账户客户端通过提供id访问的、返回PII/财务/审计数据的公开查询
  • 严格复用content/convex-expert.md中的requireIdentity/requireOwner实现 — 请勿编写并行的辅助函数或创建新的错误语义。
  • 加固后必须通过tsc验证;未通过类型检查的修复不得交付。
  • 这是针对性的授权检查,而非通用代码评审 — 请勿将范围扩展到性能/schema/验证器相关问题;此类问题请交给convex-reviewer处理。
  • 若项目中无convex/目录,请完全跳过此工具