Convex Security Scanner — Find public-query data leaks avatar

Convex Security Scanner — Find public-query data leaks

Pricing

Pay per usage

Go to Apify Store
Convex Security Scanner — Find public-query data leaks

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

Renzo Madueno

Maintained by Community

Actor 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() and mutation() builders that, unless you add an explicit const userId = await getAuthUserId(ctx) check at the top, will execute for any caller posting to your deployment's /api/query endpoint. 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:

  1. users:list, users:listAll — copy-pasted from "show all users" tutorials, ships to production with no guard
  2. messages:list, chats:list — same pattern, leaks every conversation
  3. orders: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:

  1. Leave inputs empty + click Run for a DEMO sample report
  2. Provide your convexUrl to 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 curl reproducers, paste-ready auth-guard snippets to add to each handler
  • Dataset rows: one structured row per finding

Sample finding

[CRITICAL] users:list — returns data anonymously
Items returned: 2,891
Sample columns: _id, _creationTime, email, name, imageUrl, clerkId, role, stripeCustomerId
Sensitive columns detected: email, clerkId, stripeCustomerId
Reproducer:
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.ts
import { 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

Built and maintained by Renzo Madueño, founder of Rotate Pilot, aviation exam-prep software. More tools on GitHub.