Surface
Example app (chatbot, noter, researcher)
Network
Mainnet (relayer.memory.walrus.xyz)
Package version
apps/chatbot @ main branch (not a published package)
What happened?
PATCH /api/vote checks for an existing vote using only messageId (no chatId scope), but the update is scoped by messageId AND chatId. This lets any user permanently and silently block another user's vote on a message.
Steps to reproduce
- Victim has a chat with visibility "public" containing message M. Attacker opens it and reads M's message id (rendered in the public chat view).
- Attacker creates/owns their own chat C_attacker.
- Attacker calls PATCH /api/vote with { chatId: C_attacker, messageId: M, type: "up" }. Auth passes since attacker owns C_attacker. No vote exists yet for M, so this INSERTs a row (chatId=C_attacker, messageId=M).
- Victim later calls PATCH /api/vote with their real chat: { chatId: C_victim, messageId: M, type: "up" }.
Expected
The victim's vote on their own message M is saved and reflected on subsequent votes.
Actual
voteMessage() finds an "existingVote" for messageId=M (the attacker's row, since the lookup ignores chatId), then runs UPDATE ... WHERE messageId=M AND chatId=C_victim, which matches zero rows. Silent no-op. API still returns 200 "Message voted", but nothing is saved. Victim can never vote on message M again.
Logs or error text
Checks
Surface
Example app (chatbot, noter, researcher)
Network
Mainnet (relayer.memory.walrus.xyz)
Package version
apps/chatbot @ main branch (not a published package)
What happened?
PATCH /api/vote checks for an existing vote using only messageId (no chatId scope), but the update is scoped by messageId AND chatId. This lets any user permanently and silently block another user's vote on a message.
Steps to reproduce
Expected
The victim's vote on their own message M is saved and reflected on subsequent votes.
Actual
voteMessage() finds an "existingVote" for messageId=M (the attacker's row, since the lookup ignores chatId), then runs UPDATE ... WHERE messageId=M AND chatId=C_victim, which matches zero rows. Silent no-op. API still returns 200 "Message voted", but nothing is saved. Victim can never vote on message M again.
Logs or error text
Checks