convex-authz
Audit and harden a Convex app's authorization: identity-from-arg impersonation, missing per-document ownership checks, public queries leaking data by a client-supplied id, and writes into a parent/container the caller doesn't own. Scans for the 4 shapes, applies requireIdentity/requireOwner, verifie
By get-convex · 20,515 installs
npx skills add get-convex/agent-skills --skill convex-authz
Source repository · Upstream listing
<! GENERATED from convex agents content/capabilities/convex authz.json — do not edit by hand.
Convex Authz Auditor/Hardener
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.
Workflow
0. 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.
1. 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.
2. 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.
3. 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).
4. 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.
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.