From 318d2882e670eb008d0a6a153920b44439527f12 Mon Sep 17 00:00:00 2001 From: Julio Araujo Date: Wed, 5 Aug 2026 15:00:01 +0200 Subject: [PATCH 1/2] fix: forgot password email DDP method doesn't have strict rate limits --- .changeset/fuzzy-ends-refuse.md | 5 +++++ .../auth/sendForgotPasswordEmail.ts | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .changeset/fuzzy-ends-refuse.md diff --git a/.changeset/fuzzy-ends-refuse.md b/.changeset/fuzzy-ends-refuse.md new file mode 100644 index 0000000000000..8ddb46f9b3fc7 --- /dev/null +++ b/.changeset/fuzzy-ends-refuse.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Adds per-client rate limiting to the unauthenticated sendForgotPasswordEmail method, matching the REST users.forgotPassword endpoint diff --git a/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts b/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts index e49c1dce0da09..044c17bbbafed 100644 --- a/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts +++ b/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts @@ -2,6 +2,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { check } from 'meteor/check'; +import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; import { SystemLogger } from '../../lib/logger/system'; @@ -44,3 +45,23 @@ Meteor.methods({ return sendForgotPasswordEmail(to); }, }); + +// This method is unauthenticated (callable over DDP and via method.callAnon), so we key the +// rate limit on `clientAddress` rather than `userId` — keying on the (always-null) anonymous +// userId would collapse every caller into a single global bucket. Correct per-client bucketing +// relies on `HTTP_FORWARDED_COUNT` being set to the real number of trusted proxies, the same +// assumption the generic DDP IP rate limit and login-attempt throttling already depend on. +// The 10/60s allowance mirrors the REST `users.forgotPassword` endpoint (which inherits the +// API_Enable_Rate_Limiter_Limit_Calls_Default / _Time_Default defaults) so the same operation +// has the same per-client allowance regardless of the path it is called through. +DDPRateLimiter.addRule( + { + type: 'method', + name: 'sendForgotPasswordEmail', + clientAddress() { + return true; + }, + }, + 10, + 60000, +); From 7ee777d20c5f2b54e6a97eab07685897259934fa Mon Sep 17 00:00:00 2001 From: Julio Araujo Date: Wed, 5 Aug 2026 15:00:55 +0200 Subject: [PATCH 2/2] Remove comments --- .../server/meteor-methods/auth/sendForgotPasswordEmail.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts b/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts index 044c17bbbafed..95fb48e595833 100644 --- a/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts +++ b/apps/meteor/server/meteor-methods/auth/sendForgotPasswordEmail.ts @@ -46,14 +46,6 @@ Meteor.methods({ }, }); -// This method is unauthenticated (callable over DDP and via method.callAnon), so we key the -// rate limit on `clientAddress` rather than `userId` — keying on the (always-null) anonymous -// userId would collapse every caller into a single global bucket. Correct per-client bucketing -// relies on `HTTP_FORWARDED_COUNT` being set to the real number of trusted proxies, the same -// assumption the generic DDP IP rate limit and login-attempt throttling already depend on. -// The 10/60s allowance mirrors the REST `users.forgotPassword` endpoint (which inherits the -// API_Enable_Rate_Limiter_Limit_Calls_Default / _Time_Default defaults) so the same operation -// has the same per-client allowance regardless of the path it is called through. DDPRateLimiter.addRule( { type: 'method',