From 368eb7a3526eff3460d65355c8741f3d8eca0b44 Mon Sep 17 00:00:00 2001 From: Gabor Javorszky Date: Thu, 22 Aug 2013 20:48:36 +0100 Subject: [PATCH] Added brute force protection to login Closes half of #468 * adds a 2 second limit until you can retry logging in, otherwise sends you a 401. * bounce: 2ms, checks the pw: 254ms on my machine * added a test to the casper suite --- core/server/controllers/admin.js | 31 ++++++++++++---- core/test/functional/admin/01_login_test.js | 39 +++++++++++++++++++-- core/test/functional/base.js | 4 +++ 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/core/server/controllers/admin.js b/core/server/controllers/admin.js index 5727d47da9..6b8d4d4044 100644 --- a/core/server/controllers/admin.js +++ b/core/server/controllers/admin.js @@ -13,7 +13,8 @@ var Ghost = require('../../ghost'), ghost = new Ghost(), dataProvider = ghost.dataProvider, adminNavbar, - adminControllers; + adminControllers, + loginSecurity = []; // TODO: combine path/navClass to single "slug(?)" variable with no prefix adminNavbar = { @@ -43,6 +44,7 @@ adminNavbar = { } }; + // TODO: make this a util or helper function setSelected(list, name) { _.each(list, function (item, key) { @@ -93,13 +95,28 @@ adminControllers = { }); }, 'auth': function (req, res) { - api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) { - req.session.user = user.id; - res.json(200, {redirect: req.body.redirect ? '/ghost/' - + decodeURIComponent(req.body.redirect) : '/ghost/'}); - }, function (error) { - res.json(401, {error: error.message}); + var currentTime = process.hrtime()[0], + denied = ''; + loginSecurity = _.filter(loginSecurity, function (ipTime) { + return (ipTime.time + 2 > currentTime); }); + denied = _.find(loginSecurity, function (ipTime) { + return (ipTime.ip === req.connection.remoteAddress); + }); + + if (!denied) { + loginSecurity.push({ip: req.connection.remoteAddress, time: process.hrtime()[0]}); + api.users.check({email: req.body.email, pw: req.body.password}).then(function (user) { + req.session.user = user.id; + res.json(200, {redirect: req.body.redirect ? '/ghost/' + + decodeURIComponent(req.body.redirect) : '/ghost/'}); + }, function (error) { + res.json(401, {error: error.message}); + }); + } else { + res.json(401, {error: 'Slow down, there are way too many login attempts!'}); + } + }, changepw: function (req, res) { api.users.changePassword({ diff --git a/core/test/functional/admin/01_login_test.js b/core/test/functional/admin/01_login_test.js index 5434b788d2..724b50bae7 100644 --- a/core/test/functional/admin/01_login_test.js +++ b/core/test/functional/admin/01_login_test.js @@ -1,4 +1,4 @@ -/*globals casper, __utils__, url, user */ +/*globals casper, __utils__, url, user, falseUser */ casper.test.begin("Ghost admin will load login page", 2, function suite(test) { casper.test.filename = "admin_test.png"; @@ -44,4 +44,39 @@ casper.test.begin("Can login to Ghost", 3, function suite(test) { casper.run(function () { test.done(); }); -}); \ No newline at end of file +}); + +casper.test.begin("Can't spam it", 2, function suite(test) { + + casper.test.filename = "login_test.png"; + + casper.start(url + "ghost/login/", function testTitle() { + test.assertTitle("", "Ghost admin has no title"); + }).viewport(1280, 1024); + + casper.waitFor(function checkOpaque() { + return this.evaluate(function () { + var loginBox = document.querySelector('.login-box'); + return window.getComputedStyle(loginBox).getPropertyValue('display') === "block" + && window.getComputedStyle(loginBox).getPropertyValue('opacity') === "1"; + }); + }, function then() { + this.fill("#login", falseUser, true); + casper.wait(200, function doneWait() { + this.fill("#login", falseUser, true); + }); + + }); + casper.wait(200, function doneWait() { + this.echo("I've waited for 1 seconds."); + }); + + casper.then(function testForErrorMessage() { + test.assertSelectorHasText('.notification-error', 'Slow down, there are way too many login attempts!'); + }); + + casper.run(function () { + test.done(); + }); +}); + diff --git a/core/test/functional/base.js b/core/test/functional/base.js index d4fcee1867..6e9eaebee5 100644 --- a/core/test/functional/base.js +++ b/core/test/functional/base.js @@ -31,6 +31,10 @@ var host = casper.cli.options.host || 'localhost', email: email, password: password }, + falseUser = { + email: email, + password: 'letmethrough' + }, testPost = { title: "Bacon ipsum dolor sit amet", content: "I am a test post.\n#I have some small content"