Skip to content

Unauthenticated cross-tenant read of all users' alert rules via public Convex query getByEnabled

High
koala73 published GHSA-r649-4cqj-w93h Jul 4, 2026

Package

koala73/worldmonitor

Affected versions

<= 2.8.0 (main @ commit 16d0a12e)

Patched versions

None

Description

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

CVE ID

No known CVE

Weaknesses

Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources. Learn more on MITRE.

Authorization Bypass Through User-Controlled Key

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. Learn more on MITRE.

Credits