Describe the bug
The /api/notify route (app/api/notify/route.ts) issues and verifies a notification management token to allow users to update or delete their notification preferences without re-authenticating via GitHub OAuth.
The token is hashed before storage using hashNotificationManagementToken (from lib/notification-management-token.ts). However, inspection of the flow reveals two compounding problems:
-
No per-record salt is used when hashing. If the underlying hash function is a fast, unsalted algorithm (e.g. plain SHA-256 without a cost factor), an attacker who obtains a dump of the Notification collection can pre-compute a rainbow table over the short token space and recover tokens in bulk offline.
-
The managementTokenHash field is excluded from normal projections (select('+managementTokenHash') is called explicitly), but this exclusion only prevents accidental exposure through application logic — it offers no protection against database-level access (e.g. a misconfigured Atlas IP allowlist, a compromised admin credential, or a MongoDB injection vulnerability elsewhere in the app).
Once an attacker recovers a valid token, they can silently change or delete another user's notification email address by calling POST /api/notify or DELETE /api/notify with the Authorization header set to the recovered token — no GitHub OAuth interaction required.
Steps to Reproduce
- Register notification preferences for a victim account via
POST /api/notify. Capture the managementToken returned in the response.
- Obtain a read-replica or backup dump of the
notifications collection (simulating insider access or a misconfigured Atlas cluster).
- Run an offline dictionary/brute-force attack against the stored
managementTokenHash values.
- Use a recovered token to silently overwrite the victim's email via
POST /api/notify — the server returns 200 without any additional verification.
Expected Behavior
- Token hashes should be produced with a slow, salted KDF (e.g.
bcrypt, argon2id, or scrypt) so that offline brute-force is computationally infeasible.
- Alternatively, tokens should be long enough (≥ 128 bits of entropy) that brute-force is impractical regardless of the hash algorithm.
Affected Code
lib/notification-management-token.ts — hashNotificationManagementToken and verifyNotificationManagementToken functions.
app/api/notify/route.ts, lines 198–204 — token issuance and hash storage path.
Impact
An attacker with read access to the MongoDB notifications collection (e.g. through a misconfigured connection string, a backup leak, or an injection flaw) can recover management tokens and hijack notification preferences for any registered user, including redirecting commit/streak/milestone alert emails to an attacker-controlled address.
Suggested Fix
Replace the current hash function with bcrypt (cost ≥ 12) or argon2id:
import bcrypt from 'bcryptjs';
export function hashNotificationManagementToken(token: string): string {
return bcrypt.hashSync(token, 12); // salted, adaptive cost
}
export function verifyNotificationManagementToken(token: string | null, hash: string | null): boolean {
if (!token || !hash) return false;
return bcrypt.compareSync(token, hash);
}
Environment
- Next.js 15 (App Router)
- MongoDB (Mongoose) data layer
- Deployed on Vercel (production)
Describe the bug
The
/api/notifyroute (app/api/notify/route.ts) issues and verifies a notification management token to allow users to update or delete their notification preferences without re-authenticating via GitHub OAuth.The token is hashed before storage using
hashNotificationManagementToken(fromlib/notification-management-token.ts). However, inspection of the flow reveals two compounding problems:No per-record salt is used when hashing. If the underlying hash function is a fast, unsalted algorithm (e.g. plain SHA-256 without a cost factor), an attacker who obtains a dump of the
Notificationcollection can pre-compute a rainbow table over the short token space and recover tokens in bulk offline.The
managementTokenHashfield is excluded from normal projections (select('+managementTokenHash')is called explicitly), but this exclusion only prevents accidental exposure through application logic — it offers no protection against database-level access (e.g. a misconfigured Atlas IP allowlist, a compromised admin credential, or a MongoDB injection vulnerability elsewhere in the app).Once an attacker recovers a valid token, they can silently change or delete another user's notification email address by calling
POST /api/notifyorDELETE /api/notifywith theAuthorizationheader set to the recovered token — no GitHub OAuth interaction required.Steps to Reproduce
POST /api/notify. Capture themanagementTokenreturned in the response.notificationscollection (simulating insider access or a misconfigured Atlas cluster).managementTokenHashvalues.POST /api/notify— the server returns 200 without any additional verification.Expected Behavior
bcrypt,argon2id, orscrypt) so that offline brute-force is computationally infeasible.Affected Code
lib/notification-management-token.ts—hashNotificationManagementTokenandverifyNotificationManagementTokenfunctions.app/api/notify/route.ts, lines 198–204 — token issuance and hash storage path.Impact
An attacker with read access to the MongoDB
notificationscollection (e.g. through a misconfigured connection string, a backup leak, or an injection flaw) can recover management tokens and hijack notification preferences for any registered user, including redirecting commit/streak/milestone alert emails to an attacker-controlled address.Suggested Fix
Replace the current hash function with
bcrypt(cost ≥ 12) orargon2id:Environment