diff --git a/CHANGELOG.md b/CHANGELOG.md index 709f643..53ef193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Update default auth endpoints to match current Accounts token storage (see #79) - Return "Unauthorized" for failed authentication - To match Meteor, store password token as `hashedToken` +- When logging in with bad credentials, randomly delay the response (See #81) - Add unit tests for authentication diff --git a/lib/route.coffee b/lib/route.coffee index 682fa86..aa9abbb 100644 --- a/lib/route.coffee +++ b/lib/route.coffee @@ -207,8 +207,19 @@ class @Route # Send response endpointContext.response.writeHead statusCode, headers endpointContext.response.write body - endpointContext.response.end() - + if statusCode in [401, 403] + # Hackers can measure the response time to determine things like whether the 401 response was + # caused by bad user id vs bad password. + # In doing so, they can first scan for valid user ids regardless of valid passwords. + # Delay by a random amount to reduce the ability for a hacker to determine the response time. + # See https://www.owasp.org/index.php/Blocking_Brute_Force_Attacks#Finding_Other_Countermeasures + # See https://en.wikipedia.org/wiki/Timing_attack + minimumDelayInMilliseconds = 500 + randomMultiplierBetweenOneAndTwo = 1 + Math.random() + delayInMilliseconds = minimumDelayInMilliseconds * randomMultiplierBetweenOneAndTwo + Meteor.setTimeout endpointContext.response.end, delayInMilliseconds + else + endpointContext.response.end() ### Return the object with all of the keys converted to lowercase diff --git a/test/authentication_tests.coffee b/test/authentication_tests.coffee index 5b7e2ce..d11b138 100644 --- a/test/authentication_tests.coffee +++ b/test/authentication_tests.coffee @@ -50,7 +50,9 @@ Meteor.startup -> next() - it 'should not allow a user with wrong password to login', (test, next) -> + it 'should not allow a user with wrong password to login and should respond after 500 msec', (test, next) -> + # This test should take 500 msec or more. To speed up testing, these two tests have been combined. + startTime = new Date() HTTP.post Meteor.absoluteUrl('/api/v1/login'), { data: user: username @@ -59,6 +61,8 @@ Meteor.startup -> response = JSON.parse result.content test.equal result.statusCode, 403 test.equal response.status, 'error' + durationInMilliseconds = new Date() - startTime + test.isTrue durationInMilliseconds >= 500 next() @@ -73,10 +77,11 @@ Meteor.startup -> test.equal response.status, 'success' next() - it 'should remove the logout token after logging out', (test, next) -> + it 'should remove the logout token after logging out and should respond after 500 msec', (test, next) -> Restivus.addRoute 'prevent-access-after-logout', {authRequired: true}, get: -> true - + # This test should take 500 msec or more. To speed up testing, these two tests have been combined. + startTime = new Date() HTTP.get Meteor.absoluteUrl('/api/v1/prevent-access-after-logout'), { headers: 'X-User-Id': userId @@ -86,6 +91,8 @@ Meteor.startup -> test.isTrue error test.equal result.statusCode, 401 test.equal response.status, 'error' + durationInMilliseconds = new Date() - startTime + test.isTrue durationInMilliseconds >= 500 next() it 'should allow a second logged in user to logout', (test, next) ->