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, 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.
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
/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.
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:\sv.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.)
(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
.
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.
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.
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).
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.
Show more
Installs
911
Repository
get-convex/agent-skills
GitHub Stars
42
First Seen
2 days ago
Security Audits
Gen Agent Trust Hub
Pass
Socket
Pass
Snyk
Pass