Summary
The Convex function getByEnabled in convex/alertRules.ts is exported with the public query() constructor and performs no authentication and no per-user ownership scoping. Any unauthenticated client that knows the (non-secret) Convex deployment URL can call api.alertRules.getByEnabled and receive every user's alert-rule records. This is a Broken Object-Level Authorization / missing-authentication issue (CWE-639 / CWE-306) leading to a cross-tenant disclosure of all users' data.
Details
getByEnabled (convex/alertRules.ts:665-673) is declared with the public query({...}) constructor rather than internalQuery, so it is reachable on the public api.alertRules.* surface. Its handler:
- does not call
ctx.auth.getUserIdentity(), and
- does not scope results to the caller — it runs
.withIndex("by_enabled", …).collect() and returns every matching row across all users.
Why this is reachable, not theoretical:
- The deployment URL is not secret. The frontend reads
import.meta.env.VITE_CONVEX_URL (src/services/convex-client.ts:37), which Vite inlines into the shipped public bundle.
- The repo's own admin scripts are a working unauthenticated PoC. Both
scripts/disable-free-user-notifications.mjs and scripts/migrate-realtime-high-to-daily.mjs construct a bare new ConvexHttpClient(CONVEX_URL) and call getByEnabled without ever calling .setAuth() — direct proof the query resolves with zero credentials.
- The safe pattern was known and simply omitted here. Sibling
getDigestRules (line 652) is correctly declared internalQuery, and getAlertRules (line 137) correctly gates on getUserIdentity() + by_user scope.
PoC
Against a local Convex backend (no production system contacted):
npx convex dev # note the deployment URL (or read VITE_CONVEX_URL from the public bundle)
CONVEX_URL="https://<deployment>.convex.cloud" node -e \
"const{ConvexHttpClient}=require('convex/browser');\
new ConvexHttpClient(process.env.CONVEX_URL)\
.query('alertRules:getByEnabled',{enabled:true}).then(r=>console.log(r))"
# -> full cross-tenant dump of every user's enabled alert rules.
# Repeat with {enabled:false} to dump disabled rules.
A self-contained static + in-repo proof (asserting the public constructor, the missing auth call, and the credential-free script call path) is available in the coordinated-disclosure package as poc-F1-convex-bola-alertrules.mjs.
Impact
Anonymous, cross-tenant disclosure of every user's alert-rule records, including: Clerk userId (subject identifier), countries watchlist (an analyst's monitored conflict zones can be sensitive), eventTypes, sensitivity, notification channel types, digestTimezone / quietHoursTimezone (approximate location), and timestamps. The exposed Clerk userIds can be correlated against other endpoints. Read-only (no write, no direct account takeover), so rated High rather than Critical.
Remediation
Redeclare getByEnabled as an internalQuery (matching getDigestRules) and invoke it from the migration scripts via convex run (which authenticates with CONVEX_DEPLOY_KEY). If a public variant is genuinely required, add ctx.auth.getUserIdentity() plus by_user ownership scoping (matching getAlertRules). A tested unified-diff patch (F1-convex-bola.patch) is available in the coordinated-disclosure package.
Maintainer validation (2026-07-04): Valid historically, but no longer live on current origin/main. alertRules.getByEnabled and digest reads are internal-only, while public alert rule reads are authenticated/user-scoped; convex alert-rule visibility tests cover the contract. Published advisories cannot be closed through the GitHub API, so this is marked in-description as fixed/no longer active.
Summary
The Convex function
getByEnabledinconvex/alertRules.tsis exported with the publicquery()constructor and performs no authentication and no per-user ownership scoping. Any unauthenticated client that knows the (non-secret) Convex deployment URL can callapi.alertRules.getByEnabledand receive every user's alert-rule records. This is a Broken Object-Level Authorization / missing-authentication issue (CWE-639 / CWE-306) leading to a cross-tenant disclosure of all users' data.Details
getByEnabled(convex/alertRules.ts:665-673) is declared with the publicquery({...})constructor rather thaninternalQuery, so it is reachable on the publicapi.alertRules.*surface. Its handler:ctx.auth.getUserIdentity(), and.withIndex("by_enabled", …).collect()and returns every matching row across all users.Why this is reachable, not theoretical:
import.meta.env.VITE_CONVEX_URL(src/services/convex-client.ts:37), which Vite inlines into the shipped public bundle.scripts/disable-free-user-notifications.mjsandscripts/migrate-realtime-high-to-daily.mjsconstruct a barenew ConvexHttpClient(CONVEX_URL)and callgetByEnabledwithout ever calling.setAuth()— direct proof the query resolves with zero credentials.getDigestRules(line 652) is correctly declaredinternalQuery, andgetAlertRules(line 137) correctly gates ongetUserIdentity()+by_userscope.PoC
Against a local Convex backend (no production system contacted):
A self-contained static + in-repo proof (asserting the public constructor, the missing auth call, and the credential-free script call path) is available in the coordinated-disclosure package as
poc-F1-convex-bola-alertrules.mjs.Impact
Anonymous, cross-tenant disclosure of every user's alert-rule records, including: Clerk
userId(subject identifier),countrieswatchlist (an analyst's monitored conflict zones can be sensitive),eventTypes,sensitivity, notification channel types,digestTimezone/quietHoursTimezone(approximate location), and timestamps. The exposed ClerkuserIds can be correlated against other endpoints. Read-only (no write, no direct account takeover), so rated High rather than Critical.Remediation
Redeclare
getByEnabledas aninternalQuery(matchinggetDigestRules) and invoke it from the migration scripts viaconvex run(which authenticates withCONVEX_DEPLOY_KEY). If a public variant is genuinely required, addctx.auth.getUserIdentity()plusby_userownership scoping (matchinggetAlertRules). A tested unified-diff patch (F1-convex-bola.patch) is available in the coordinated-disclosure package.Maintainer validation (2026-07-04): Valid historically, but no longer live on current origin/main. alertRules.getByEnabled and digest reads are internal-only, while public alert rule reads are authenticated/user-scoped; convex alert-rule visibility tests cover the contract. Published advisories cannot be closed through the GitHub API, so this is marked in-description as fixed/no longer active.