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. -- By Renzo Madueno, https://rotatepilot.com
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Renzo Madueno
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days 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
FAQ
How do I check if my Convex queries leak data without authentication?
Provide your convexUrl (and optionally functionHints for custom function paths) and run the actor — or leave inputs empty for a demo report. In about 30 seconds it probes ~30 common function paths (users:list, messages:list, orders:list, etc.) and lists every query that returns data without an auth check, with item counts and a curl reproducer.
Do I need an API key to run this?
No. Convex's POST /api/query endpoint is reachable by any caller — that's exactly the exposure being tested — so the scanner needs only your deployment URL. No deploy key, admin token, or login.
Why use this scanner instead of reading my Convex functions manually?
Code review tells you which handlers should have a getAuthUserId guard; this scanner proves which ones actually run for an anonymous caller. The leaky pattern (a list query copied from a tutorial without the auth check) is easy to overlook in review — honestly, do both, but only the live probe confirms the deployment is exposed right now.
What are the alternatives to this Convex scanner? If you use a different backend, the sister scanners cover it: Supabase, Firebase, Appwrite, and Directus.
What vulnerabilities does it check for?
It detects Convex query() functions that return data without an authentication guard — the framework's public-by-default behavior — across ~30 common function paths plus any hints you pass, and flags sensitive columns (email, token, stripeCustomerId, etc.) in the returned data.
Is the scan safe and read-only?
Yes. The probe only calls existing query functions via POST /api/query; it never invokes mutations and never writes data. It reports item counts and sample column names to confirm exposure — counts only, never persisted row data.
Automate it
Auth guards get dropped when a list function is cloned for a new feature or an admin dashboard. Use Apify's scheduler to re-scan your Convex deployment nightly or weekly, then connect the output via Apify integrations — Slack, Make, n8n, Zapier, or a webhook — so your team is alerted the moment a query starts returning data anonymously. Recurring scans turn a one-time audit into continuous monitoring.
Related
- Stripe audit ($99 one-time): buy.stripe.com/00w9AT9TWdaW7yx9KkcAo01
- Weekly auto-scans ($29/mo): rls-monitor.vercel.app
Related actors
- Supabase Security Auditor — Detect Row-Level-Security misconfigurations and anon-readable tables in Supabase projects.
- Firebase Security Scanner — Find public Firestore collections and Realtime Database paths readable without auth.
- Appwrite Security Scanner — Find Appwrite collections readable by the guest/
anyrole. - Directus Security Scanner — Find Directus collections readable by the Public role.
- Multi-Scraper MCP — Give an AI agent live web-data tools alongside your security workflows.
Built and maintained by Renzo Madueño, founder of Rotate Pilot, aviation exam-prep software. More tools on GitHub.


