Convex Security Scanner — Find public-query data leaks
Pricing
Pay per usage
Convex Security Scanner — Find public-query data leaks
Probes a public Convex deployment for queries that return data without auth. Convex queries are public-by-default unless you check auth inside the handler. Returns counts + reproducer.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Renzo Madueno
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
18 hours ago
Last modified
Categories
Share
Convex queries are PUBLIC BY DEFAULT. The framework gives you
query()andmutation()builders that, unless you add an explicitconst userId = await getAuthUserId(ctx)check at the top, will execute for any caller posting to your deployment's/api/queryendpoint. Every Convex starter on GitHub I've reviewed has at least one query that forgot the auth check. This actor finds those leaks in 30 seconds.
Probes a public Convex deployment for queries that return data without authentication. Sends POST /api/query with the standard Convex protocol per function path. Returns item counts + a verbatim curl reproducer per finding. Counts only — never row data persisted.
💸 Found a leak? I do turnkey Convex audits + auth-guard rewrites for $99 (Stripe — 48h, money-back if nothing actionable). Or $29/mo weekly auto-scans at rls-monitor.vercel.app.
⭐️ Solo dev competing with bigger Apify publishers. A 30-second review is the single thing that lifts ranking. Thank you.
Why this exists
Convex's mental model is functions-as-API. You write query() handlers in TypeScript, and the framework deploys them. The default behavior is no auth required — the handler runs for every caller. To require auth you must explicitly write:
const userId = await getAuthUserId(ctx);if (!userId) throw new Error('Unauthorized');
This works great when you remember. The problem: tutorials show queries without auth checks (to keep examples simple), and that pattern bleeds into real code. Worst offenders:
users:list,users:listAll— copy-pasted from "show all users" tutorials, ships to production with no guardmessages:list,chats:list— same pattern, leaks every conversationorders:list,payments:list— when the team builds an admin dashboard, they often clone an existing list function and forget the role check
This scanner probes ~30 common Convex function paths (users:list, messages:list, etc.) plus any custom paths you pass as hints.
How to run
Either:
- Leave inputs empty + click Run for a DEMO sample report
- Provide your
convexUrlto scan your actual deployment
{"convexUrl": "https://abc-fox-456.convex.cloud","functionHints": ["custom:listForUser", "myFile:getSomething"],"outputFormat": "both"}
What you get
- HTML report in run's KV store: severity-coded findings, copy-pasteable
curlreproducers, paste-ready auth-guard snippets to add to each handler - Dataset rows: one structured row per finding
Sample finding
[CRITICAL] users:list — returns data anonymouslyItems returned: 2,891Sample columns: _id, _creationTime, email, name, imageUrl, clerkId, role, stripeCustomerIdSensitive columns detected: email, clerkId, stripeCustomerIdReproducer:curl -X POST 'https://abc-fox-456.convex.cloud/api/query' \\-H 'Content-Type: application/json' \\-d '{"path":"users:list","args":{}}'
How to fix (code change)
In each leaky query, add an auth guard at the top of the handler:
// convex/users.tsimport { v } from 'convex/values';import { query } from './_generated/server';import { getAuthUserId } from '@convex-dev/auth/server';export const list = query({args: {},handler: async (ctx) => {const userId = await getAuthUserId(ctx);if (!userId) throw new Error('Unauthorized');return await ctx.db.query('users').collect();},});
For per-user scoped reads (most common case):
handler: async (ctx) => {const userId = await getAuthUserId(ctx);if (!userId) throw new Error('Unauthorized');return await ctx.db.query('orders').withIndex('byOwner', q => q.eq('ownerId', userId)).collect();}
For role-gated reads (admin dashboard):
handler: async (ctx) => {const userId = await getAuthUserId(ctx);if (!userId) throw new Error('Unauthorized');const me = await ctx.db.get(userId);if (me?.role !== 'admin') throw new Error('Forbidden');return await ctx.db.query('orders').collect();}
Ethical use
- Only scan deployments you own
- Probe queries do not write data; they only call existing queries
Related
- Stripe audit ($99 one-time): buy.stripe.com/00w9AT9TWdaW7yx9KkcAo01
- Weekly auto-scans ($29/mo): rls-monitor.vercel.app
- Sister scanners: Supabase, Firebase, Strapi, Directus, Payload CMS, Convex, Hasura, PocketBase, Appwrite, Nhost.
Built by Renzo.