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.

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

18 hours 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 by Renzo.