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

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.

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