fix(security): POST /api/auth/update trusted a client-supplied pubkey (IDOR)
Found live while wiring the "existing bot" AI-config settings UI: this route parsed `pubkey` from the request body and used it directly to select which bot row to update, with no check that it matched the caller's actual authenticated identity. Any unauthenticated caller could POST an arbitrary victim's pubkey plus a malicious webhookUrl, profilePicUrl, or customization payload and silently hijack that bot (e.g. redirect its webhook to an attacker-controlled endpoint). Contrast with GET /me and POST /regenerate-secret, which both correctly derive pubkey from the verified JWT via extractPubkeyFromAuth and never trust a client-claimed identity — this was the one route that didn't follow that pattern. Fixed by deriving pubkey from the JWT exclusively; updateBotSchema no longer declares a pubkey field at all (was the only schema-level signal that the vulnerable code path existed). Frontend callers updated to stop sending a pubkey they no longer need. Added a regression suite (auth-update.test.ts) covering: 401 with no/garbage auth, hijack-attempt-via-body-pubkey now 404s and leaves the victim's row untouched, and legitimate self-updates still work when the body happens to carry an unrelated pubkey field (ignored, not trusted). Full server suite: 829/829 passing. tsc --noEmit clean (server + frontend). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -132,14 +132,22 @@ describe('registerHumanSchema', () => {
|
||||
})
|
||||
|
||||
describe('updateBotSchema', () => {
|
||||
const base = { pubkey: 'a'.repeat(64) }
|
||||
it('accepts pubkey only (no updates)', () => { expect(updateBotSchema.safeParse(base).success).toBe(true) })
|
||||
const base = {}
|
||||
it('accepts empty body (no updates)', () => { expect(updateBotSchema.safeParse(base).success).toBe(true) })
|
||||
it('accepts webhook update', () => {
|
||||
expect(updateBotSchema.safeParse({ ...base, webhookUrl: 'https://new.com/hook' }).success).toBe(true)
|
||||
})
|
||||
it('rejects file:// webhook', () => {
|
||||
expect(updateBotSchema.safeParse({ ...base, webhookUrl: 'file:///etc/passwd' }).success).toBe(false)
|
||||
})
|
||||
// SECURITY REGRESSION: pubkey must never be a schema field here. POST
|
||||
// /api/auth/update derives identity from the verified JWT
|
||||
// (extractPubkeyFromAuth), not from client body — see server/src/routes/auth.ts.
|
||||
// A pubkey field in this schema previously let an unauthenticated caller
|
||||
// claim any bot as their own and hijack its webhook/customization.
|
||||
it('does not declare a pubkey field (identity comes from the JWT, not the body)', () => {
|
||||
expect('pubkey' in updateBotSchema.shape).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Fight schemas ---
|
||||
|
||||
Reference in New Issue
Block a user