@@ -177,6 +177,8 @@ def relocate_reclaim(request, user_id):
177
177
@set_referrer_policy ("strict-origin-when-cross-origin" )
178
178
@control_silo_function
179
179
def recover_confirm (request , user_id , hash , mode = "recover" ):
180
+ from sentry import ratelimits as ratelimiter
181
+
180
182
try :
181
183
password_hash = LostPasswordHash .objects .get (user = user_id , hash = hash )
182
184
if not password_hash .is_valid ():
@@ -186,6 +188,24 @@ def recover_confirm(request, user_id, hash, mode="recover"):
186
188
except LostPasswordHash .DoesNotExist :
187
189
return render_to_response (get_template (mode , "failure" ), {"user_id" : user_id }, request )
188
190
191
+ extra = {
192
+ "ip_address" : request .META ["REMOTE_ADDR" ],
193
+ "user_agent" : request .META .get ("HTTP_USER_AGENT" ),
194
+ }
195
+
196
+ if request .method == "POST" and ratelimiter .backend .is_limited (
197
+ "accounts:confirm:{}" .format (extra ["ip_address" ]),
198
+ limit = 5 ,
199
+ window = 60 , # 5 per minute should be enough for anyone
200
+ ):
201
+ logger .warning ("confirm.rate-limited" , extra = extra )
202
+
203
+ return HttpResponse (
204
+ "You have made too many attempts. Please try again later." ,
205
+ content_type = "text/plain" ,
206
+ status = 429 ,
207
+ )
208
+
189
209
# TODO(getsentry/team-ospo#190): Clean up ternary logic and only show relocation form if user is unclaimed
190
210
form_cls = RelocationForm if mode == "relocate" else ChangePasswordRecoverForm
191
211
if request .method == "POST" :
0 commit comments