close three cross-account holes found reading the multi-user path
reset-password accepted any valid signed jwt as a reset token, including a 30-day session token — its sibling verify-token.ts already gated on purpose === 'reset-password' and this handler did not. forgot-password mints that claim, so the gate costs the legitimate flow nothing. notify's DELETE /_officer/devices/:token deleted by token with no user predicate: a token is the address of a device, not a secret, so any account holding the notify capability could deregister another's device. deletePushDevice now takes an optional userId — the route passes it, the APNs/FCM dead-token paths deliberately do not. POST /_officer/notify let a request body's userId override the proxy-injected X-Officer-User. The header now wins where present, which is what separates a signed-in browser from a loopback producer that has no session to speak from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,9 +37,21 @@ export async function getPushDevices(userId: number, appSlug?: string): Promise<
|
||||
return db.select().from(pushDevices).where(where);
|
||||
}
|
||||
|
||||
/** Sign-out, or a hard rejection from Apple/Google (410 Unregistered, UNREGISTERED). */
|
||||
export async function deletePushDevice(token: string): Promise<void> {
|
||||
await db.delete(pushDevices).where(eq(pushDevices.token, token));
|
||||
/**
|
||||
* Sign-out, or a hard rejection from Apple/Google (410 Unregistered, UNREGISTERED).
|
||||
*
|
||||
* `userId` is optional because the two callers are genuinely different. The APNs/FCM paths delete a
|
||||
* token the provider has just declared dead, which is true for whoever owns it — they pass nothing. The
|
||||
* `DELETE /_officer/devices/:token` route is reached by a signed-in account naming a token in the URL,
|
||||
* so it MUST pass its own id: a token is the address of a device, not a secret, and without the
|
||||
* predicate any account could deregister another's device by guessing or replaying one.
|
||||
*/
|
||||
export async function deletePushDevice(token: string, userId?: number): Promise<void> {
|
||||
const where =
|
||||
userId === undefined
|
||||
? eq(pushDevices.token, token)
|
||||
: and(eq(pushDevices.token, token), eq(pushDevices.userId, userId));
|
||||
await db.delete(pushDevices).where(where);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user