fix(backend): rate limit the password-forgot endpoint - #568
Merged
Conversation
POST /api/auth/password/forgot was unthrottled: anyone could trigger unlimited reset emails to a known address (mail bomb). Login had throttling; this endpoint did not. Adds a fixed_window limiter (3 per 15 min) consumed per client IP and per target email; exceeding either returns 429 before any mail is sent. The admin-triggered reset path is deliberately not throttled. Test env gets a high limit so the suite stays hermetic.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Security audit finding #6:
POST /api/auth/password/forgotwas public and unthrottled — anyone could trigger unlimited password-reset emails to a known address (mail bomb / harassment). Login hadlogin_throttling; this endpoint had nothing.Fix
password_forgotrate limiter (config/packages/rate_limiter.yaml): fixed window, 3 per 15 minutes.PasswordController::forgotPasswordconsumes the limiter twice — keyed by client IP (generic flooding) and by target email (targeted mail-bombing from rotating IPs) — and throws 429 before any mail is sent. 429 flows through the existingHttpExceptionHandler, so the JSON error shape is unchanged.Admin\UserManagementController) deliberately bypasses the limiter.when@testraises the limit to keep the test suite hermetic (limiter storage iscache.app, which persists across runs).Implementation note: this Symfony version registers no
RateLimiterFactory $passwordForgotLimiternamed alias, so injection is by service id (#[Autowire(service: 'limiter.password_forgot')]).Tests
PasswordForgotThrottleTest(unit, realRateLimiterFactory+InMemoryStorage): IP-budget exhaustion and email-budget exhaustion each yield 429 and the reset service is never called.testForgotPasswordIsAccepted: legit request still succeeds and the (hashed) reset token is persisted.