Browse Source

Merge pull request #1383 from sharelatex/spd-rate-limits

Add rate limits to email-related endpoints

GitOrigin-RevId: 05a8b40eb65a55aba35788e2401e6988b672b389
Simon Detheridge 7 years ago
parent
commit
4360a55fdc

+ 9 - 5
services/web/app/coffee/Features/Collaborators/CollaboratorsInviteController.coffee

@@ -101,11 +101,15 @@ module.exports = CollaboratorsInviteController =
 		inviteId = req.params.invite_id
 		logger.log {projectId, inviteId}, "resending invite"
 		sendingUser = AuthenticationController.getSessionUser(req)
-		CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
-			if err?
-				logger.err {projectId, inviteId}, "error resending invite"
-				return next(err)
-			res.sendStatus(201)
+		CollaboratorsInviteController._checkRateLimit sendingUser._id, (error, underRateLimit) ->
+			return next(error) if error?
+			if !underRateLimit
+				return res.sendStatus(429)
+			CollaboratorsInviteHandler.resendInvite projectId, sendingUser, inviteId, (err) ->
+				if err?
+					logger.err {projectId, inviteId}, "error resending invite"
+					return next(err)
+				res.sendStatus(201)
 
 	viewInvite: (req, res, next) ->
 		projectId = req.params.Project_id

+ 1 - 1
services/web/app/coffee/Features/PasswordReset/PasswordResetController.coffee

@@ -21,7 +21,7 @@ module.exports =
 			throttle: 6
 		RateLimiter.addCount opts, (err, canContinue)->
 			if !canContinue
-				return res.send 500, { message: req.i18n.translate("rate_limit_hit_wait")}
+				return res.send 429, { message: req.i18n.translate("rate_limit_hit_wait")}
 			PasswordResetHandler.generateAndEmailResetToken email, (err, exists)->
 				if err?
 					res.send 500, {message:err?.message}

+ 16 - 2
services/web/app/coffee/Features/Subscription/SubscriptionRouter.coffee

@@ -3,6 +3,7 @@ SubscriptionController = require('./SubscriptionController')
 SubscriptionGroupController = require './SubscriptionGroupController'
 DomainLicenceController = require './DomainLicenceController'
 TeamInvitesController = require './TeamInvitesController'
+RateLimiterMiddlewear = require('../Security/RateLimiterMiddlewear')
 Settings = require "settings-sharelatex"
 
 module.exports =
@@ -24,12 +25,25 @@ module.exports =
 		# Team invites
 		webRouter.get '/subscription/invites/:token/',  AuthenticationController.requireLogin(),
 			TeamInvitesController.viewInvite
-		webRouter.put '/subscription/invites/:token/',  AuthenticationController.requireLogin(),
+		webRouter.put '/subscription/invites/:token/',
+			AuthenticationController.requireLogin(),
+			RateLimiterMiddlewear.rateLimit({
+				endpointName: 'team-invite',
+				maxRequests: 10
+				timeInterval: 60
+			}),
 			TeamInvitesController.acceptInvite
 
 		# Routes to join a domain licence team
 		webRouter.get '/user/subscription/domain/join', AuthenticationController.requireLogin(), DomainLicenceController.join
-		webRouter.post '/user/subscription/domain/join', AuthenticationController.requireLogin(), DomainLicenceController.createInvite
+		webRouter.post '/user/subscription/domain/join',
+			AuthenticationController.requireLogin(),
+			RateLimiterMiddlewear.rateLimit({
+				endpointName: 'join-domain-subscription',
+				maxRequests: 10
+				timeInterval: 60
+			}),
+			DomainLicenceController.createInvite
 
 		#recurly callback
 		publicApiRouter.post '/user/subscription/callback',   SubscriptionController.recurlyNotificationParser, SubscriptionController.recurlyCallback

+ 1 - 0
services/web/app/coffee/Features/User/UserEmailsConfirmationHandler.coffee

@@ -21,6 +21,7 @@ module.exports = UserEmailsConfirmationHandler =
 			emailOptions =
 				to: email
 				confirmEmailUrl: "#{settings.siteUrl}/user/emails/confirm?token=#{token}"
+				sendingUser_id: user_id
 			EmailHandler.sendEmail emailTemplate, emailOptions, callback
 
 	confirmEmailFromToken: (token, callback = (error) ->) ->

+ 13 - 2
services/web/app/coffee/router.coffee

@@ -122,6 +122,11 @@ module.exports = class Router
 			UserEmailsController.confirm
 		webRouter.post '/user/emails/resend_confirmation',
 			AuthenticationController.requireLogin(),
+			RateLimiterMiddlewear.rateLimit({
+				endpointName: "resend-confirmation"
+				maxRequests: 10
+				timeInterval: 60
+			}),
 			UserEmailsController.resendConfirmation
 
 		if Features.hasFeature 'affiliations'
@@ -326,8 +331,14 @@ module.exports = class Router
 		# webRouter.post "/beta/opt-in", AuthenticationController.requireLogin(), BetaProgramController.optIn
 		# webRouter.post "/beta/opt-out", AuthenticationController.requireLogin(), BetaProgramController.optOut
 		webRouter.get "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.sudoModePrompt
-		webRouter.post "/confirm-password", AuthenticationController.requireLogin(), SudoModeController.submitPassword
-
+		webRouter.post "/confirm-password",
+			AuthenticationController.requireLogin(),
+			RateLimiterMiddlewear.rateLimit({
+				endpointName: "confirm-password"
+				maxRequests: 10
+				timeInterval: 60
+			}),
+			SudoModeController.submitPassword
 
 		# New "api" endpoints. Started as a way for v1 to call over to v2 (for
 		# long-term features, as opposed to the nominally temporary ones in the

+ 4 - 0
services/web/test/unit/coffee/Collaborators/CollaboratorsInviteControllerTests.coffee

@@ -570,6 +570,7 @@ describe "CollaboratorsInviteController", ->
 			@res.render = sinon.stub()
 			@res.sendStatus = sinon.stub()
 			@CollaboratorsInviteHandler.resendInvite = sinon.stub().callsArgWith(3, null)
+			@CollaboratorsInviteController._checkRateLimit = sinon.stub().yields(null, true)
 			@callback = sinon.stub()
 			@next = sinon.stub()
 
@@ -585,6 +586,9 @@ describe "CollaboratorsInviteController", ->
 			it 'should have called resendInvite', ->
 				@CollaboratorsInviteHandler.resendInvite.callCount.should.equal 1
 
+			it 'should check the rate limit', ->
+				@CollaboratorsInviteController._checkRateLimit.callCount.should.equal 1
+
 		describe 'when resendInvite produces an error', ->
 
 			beforeEach ->

+ 1 - 1
services/web/test/unit/coffee/PasswordReset/PasswordResetControllerTests.coffee

@@ -51,7 +51,7 @@ describe "PasswordResetController", ->
 			@PasswordResetHandler.generateAndEmailResetToken.callsArgWith(1, null, true)
 			@RateLimiter.addCount.callsArgWith(1, null, false)
 			@res.send = (code)=>
-				code.should.equal 500
+				code.should.equal 429
 				@PasswordResetHandler.generateAndEmailResetToken.calledWith(@email.trim()).should.equal false
 				done()
 			@PasswordResetController.requestReset @req, @res

+ 1 - 0
services/web/test/unit/coffee/User/UserEmailsConfirmationHandlerTests.coffee

@@ -46,6 +46,7 @@ describe "UserEmailsConfirmationHandler", ->
 					.calledWith('confirmEmail', {
 						to: @email,
 						confirmEmailUrl: 'emails.example.com/user/emails/confirm?token=new-token'
+						sendingUser_id: @user_id
 					})
 					.should.equal true