From e9f68e65b7c491d36c02fc426261b9a6933ddcb7 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 28 Mar 2023 22:48:42 +0200 Subject: [PATCH] server: fix rate limit for adding reactions Adding a reaction may delete a previous reaction to the same note, thus consequently this needs to be in the rate limiting group if this happens. Otherwise the rate limit can be circumvented. Changelog: Fixed --- .../api/endpoints/notes/reactions/create.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts index fd6f2bc04..8ed1ca7aa 100644 --- a/packages/backend/src/server/api/endpoints/notes/reactions/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/reactions/create.ts @@ -2,15 +2,20 @@ import { createReaction } from '@/services/note/reaction/create.js'; import define from '@/server/api/define.js'; import { getNote } from '@/server/api/common/getters.js'; import { ApiError } from '@/server/api/error.js'; +import { HOUR, SECOND } from '@/const.js'; +import { limiter } from '@/server/api/limiter.js'; +import { NoteReactions } from '@/models/index.js'; export const meta = { tags: ['reactions', 'notes'], + description: 'Add a reaction to a note. If there already is a reaction to this note, deletes it and is consequently subject to the `delete` rate limiting group as if using `notes/reactions/delete`.', + requireCredential: true, kind: 'write:reactions', - errors: ['NO_SUCH_NOTE', 'ALREADY_REACTED', 'BLOCKED'], + errors: ['NO_SUCH_NOTE', 'ALREADY_REACTED', 'BLOCKED', 'RATE_LIMIT_EXCEEDED'], } as const; export const paramDef = { @@ -24,10 +29,27 @@ export const paramDef = { // eslint-disable-next-line import/no-default-export export default define(meta, paramDef, async (ps, user) => { - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); + const [note, reactionCount] = await Promise.all([ + getNote(ps.noteId, user).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); + throw err; + }), + NoteReactions.countBy({ + noteId: ps.noteId, + userId: user.id, + }), + ]); + + if (reactionCount > 0) { + const limit = { + key: 'delete', + duration: HOUR, + max: 30, + minInterval: 10 * SECOND, + }; + await limiter(limit, user.id); + } + await createReaction(user, note, ps.reaction).catch(e => { if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError('ALREADY_REACTED'); if (e.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError('BLOCKED');