mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Merge pull request #3595 from sebgie/issue#3544
Improve spam prevention
This commit is contained in:
commit
014f16991e
13 changed files with 83 additions and 88 deletions
|
@ -82,8 +82,6 @@ config = {
|
|||
host: '127.0.0.1',
|
||||
port: '2369'
|
||||
},
|
||||
ratePeriod: 1,
|
||||
spamTimeout: 5,
|
||||
logging: false
|
||||
},
|
||||
|
||||
|
@ -105,8 +103,6 @@ config = {
|
|||
host: '127.0.0.1',
|
||||
port: '2369'
|
||||
},
|
||||
ratePeriod: 1,
|
||||
spamTimeout: 5,
|
||||
logging: false
|
||||
},
|
||||
|
||||
|
@ -124,8 +120,6 @@ config = {
|
|||
charset : 'utf8'
|
||||
}
|
||||
},
|
||||
ratePeriod: 1,
|
||||
spamTimeout: 5,
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: '2369'
|
||||
|
|
|
@ -232,7 +232,7 @@ setupMiddleware = function (server) {
|
|||
expressServer = server;
|
||||
middleware.cacheServer(expressServer);
|
||||
middleware.cacheOauthServer(oauthServer);
|
||||
oauth.init(oauthServer);
|
||||
oauth.init(oauthServer, middleware.resetSpamCounter);
|
||||
|
||||
// Make sure 'req.secure' is valid for proxied requests
|
||||
// (X-Forwarded-Proto header will be checked, if present)
|
||||
|
|
|
@ -14,7 +14,8 @@ var _ = require('lodash'),
|
|||
|
||||
expressServer,
|
||||
oauthServer,
|
||||
loginSecurity = [];
|
||||
loginSecurity = [],
|
||||
forgottenSecurity = [];
|
||||
|
||||
function isBlackListedFileType(file) {
|
||||
var blackListedFileTypes = ['.hbs', '.md', '.json'],
|
||||
|
@ -132,44 +133,90 @@ var middleware = {
|
|||
express['static'](path.join(config.paths.themePath, activeTheme.value), {maxAge: utils.ONE_YEAR_MS})(req, res, next);
|
||||
});
|
||||
},
|
||||
|
||||
// ### Spam prevention Middleware
|
||||
// limit signin requests to one every two seconds
|
||||
spamPrevention: function (req, res, next) {
|
||||
// limit signin requests to ten failed requests per IP per hour
|
||||
spamSigninPrevention: function (req, res, next) {
|
||||
var currentTime = process.hrtime()[0],
|
||||
remoteAddress = req.connection.remoteAddress,
|
||||
deniedSpam = '',
|
||||
deniedRateLimit = '',
|
||||
ipCount = '',
|
||||
spamTimeout = config.spamTimeout || 2,
|
||||
ratePeriod = config.ratePeriod || 3600,
|
||||
rateAttempts = config.rateAttempts || 5;
|
||||
rateSigninPeriod = config.rateSigninPeriod || 3600,
|
||||
rateSigninAttempts = config.rateSigninAttempts || 10;
|
||||
|
||||
// filter for IPs that tried to login in the last 2 sec
|
||||
loginSecurity = _.filter(loginSecurity, function (logTime) {
|
||||
return (logTime.time + ratePeriod > currentTime);
|
||||
});
|
||||
|
||||
|
||||
// check if IP tried to login in the last 2 sec
|
||||
deniedSpam = _.find(loginSecurity, function (logTime) {
|
||||
return (logTime.time + spamTimeout > currentTime && logTime.ip === remoteAddress);
|
||||
});
|
||||
|
||||
// check if IP tried to login more than 'rateAttempts' time in the last 'ratePeriod' seconds
|
||||
ipCount = _.chain(loginSecurity).countBy('ip').value();
|
||||
deniedRateLimit = (ipCount[remoteAddress] > rateAttempts);
|
||||
|
||||
if ((!deniedSpam && !deniedRateLimit) || expressServer.get('disableLoginLimiter') === true) {
|
||||
loginSecurity.push({ip: remoteAddress, time: currentTime});
|
||||
next();
|
||||
if (req.body.username) {
|
||||
loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username});
|
||||
} else {
|
||||
if (deniedRateLimit) {
|
||||
return next(new errors.UnauthorizedError('Only ' + rateAttempts + ' tries per IP address every ' + ratePeriod + ' seconds.'));
|
||||
} else {
|
||||
return next(new errors.UnauthorizedError('Slow down, there are way too many login attempts!'));
|
||||
}
|
||||
return next(new errors.BadRequestError('No username.'));
|
||||
}
|
||||
|
||||
// filter entries that are older than rateSigninPeriod
|
||||
loginSecurity = _.filter(loginSecurity, function (logTime) {
|
||||
return (logTime.time + rateSigninPeriod > currentTime);
|
||||
});
|
||||
|
||||
// check number of tries per IP address
|
||||
ipCount = _.chain(loginSecurity).countBy('ip').value();
|
||||
deniedRateLimit = (ipCount[remoteAddress] > rateSigninAttempts);
|
||||
|
||||
if (deniedRateLimit) {
|
||||
return next(new errors.UnauthorizedError('Only ' + rateSigninAttempts + ' tries per IP address every ' + rateSigninPeriod + ' seconds.'));
|
||||
}
|
||||
next();
|
||||
},
|
||||
|
||||
// ### Spam prevention Middleware
|
||||
// limit forgotten password requests to five requests per IP per hour for different email addresses
|
||||
// limit forgotten password requests to five requests per email address
|
||||
spamForgottenPrevention: function (req, res, next) {
|
||||
var currentTime = process.hrtime()[0],
|
||||
remoteAddress = req.connection.remoteAddress,
|
||||
rateForgottenPeriod = config.rateForgottenPeriod || 3600,
|
||||
rateForgottenAttempts = config.rateForgottenAttempts || 5,
|
||||
email = req.body.passwordreset[0].email,
|
||||
ipCount = '',
|
||||
deniedRateLimit = '',
|
||||
deniedEmailRateLimit = '',
|
||||
index = _.findIndex(forgottenSecurity, function (logTime) {
|
||||
return (logTime.ip === remoteAddress && logTime.email === email);
|
||||
});
|
||||
|
||||
if (email) {
|
||||
if (index !== -1) {
|
||||
forgottenSecurity[index].count = forgottenSecurity[index].count + 1;
|
||||
} else {
|
||||
forgottenSecurity.push({ip: remoteAddress, time: currentTime, email: email, count: 0});
|
||||
}
|
||||
} else {
|
||||
return next(new errors.BadRequestError('No email.'));
|
||||
}
|
||||
|
||||
// filter entries that are older than rateForgottenPeriod
|
||||
forgottenSecurity = _.filter(forgottenSecurity, function (logTime) {
|
||||
return (logTime.time + rateForgottenPeriod > currentTime);
|
||||
});
|
||||
|
||||
// check number of tries with different email addresses per IP
|
||||
ipCount = _.chain(forgottenSecurity).countBy('ip').value();
|
||||
deniedRateLimit = (ipCount[remoteAddress] > rateForgottenAttempts);
|
||||
|
||||
if (index !== -1) {
|
||||
deniedEmailRateLimit = (forgottenSecurity[index].count > rateForgottenAttempts);
|
||||
}
|
||||
|
||||
if (deniedEmailRateLimit) {
|
||||
return next(new errors.UnauthorizedError('Only ' + rateForgottenAttempts + ' forgotten password attempts per email every ' + rateForgottenPeriod + ' seconds.'));
|
||||
}
|
||||
|
||||
if (deniedRateLimit) {
|
||||
return next(new errors.UnauthorizedError('Only ' + rateForgottenAttempts + ' tries per IP address every ' + rateForgottenPeriod + ' seconds.'));
|
||||
}
|
||||
|
||||
next();
|
||||
},
|
||||
resetSpamCounter: function (email) {
|
||||
loginSecurity = _.filter(loginSecurity, function (logTime) {
|
||||
return (logTime.email !== email);
|
||||
});
|
||||
},
|
||||
|
||||
// work around to handle missing client_secret
|
||||
|
|
|
@ -7,7 +7,7 @@ var oauth2orize = require('oauth2orize'),
|
|||
|
||||
oauth = {
|
||||
|
||||
init: function (oauthServer) {
|
||||
init: function (oauthServer, resetSpamCounter) {
|
||||
|
||||
// remove all expired accesstokens on startup
|
||||
models.Accesstoken.destroyAllExpired();
|
||||
|
@ -39,6 +39,7 @@ oauth = {
|
|||
return models.Accesstoken.add({token: accessToken, user_id: user.id, client_id: client.id, expires: accessExpires}).then(function () {
|
||||
return models.Refreshtoken.add({token: refreshToken, user_id: user.id, client_id: client.id, expires: refreshExpires});
|
||||
}).then(function () {
|
||||
resetSpamCounter(username);
|
||||
return done(null, accessToken, refreshToken, {expires_in: utils.ONE_HOUR_S});
|
||||
}).catch(function () {
|
||||
return done(null, false);
|
||||
|
|
|
@ -72,7 +72,7 @@ apiRoutes = function (middleware) {
|
|||
|
||||
// ## Authentication
|
||||
router.post('/authentication/passwordreset',
|
||||
middleware.spamPrevention,
|
||||
middleware.spamForgottenPrevention,
|
||||
api.http(api.authentication.generateResetToken)
|
||||
);
|
||||
router.put('/authentication/passwordreset', api.http(api.authentication.resetPassword));
|
||||
|
@ -80,7 +80,7 @@ apiRoutes = function (middleware) {
|
|||
router.post('/authentication/setup', api.http(api.authentication.setup));
|
||||
router.get('/authentication/setup', api.http(api.authentication.isSetup));
|
||||
router.post('/authentication/token',
|
||||
middleware.spamPrevention,
|
||||
middleware.spamSigninPrevention,
|
||||
middleware.addClientSecret,
|
||||
middleware.authenticateClient,
|
||||
middleware.generateAccessToken
|
||||
|
|
|
@ -30,43 +30,6 @@ CasperTest.begin('Redirects login to signin', 2, function suite(test) {
|
|||
});
|
||||
}, true);
|
||||
|
||||
// CasperTest.begin('Can\'t spam it', 4, function suite(test) {
|
||||
// CasperTest.Routines.signout.run(test);
|
||||
|
||||
// casper.thenOpenAndWaitForPageLoad('signin', function testTitle() {
|
||||
// test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
// test.assertUrlMatch(/ghost\/signin\/$/, 'Landed on the correct URL');
|
||||
// });
|
||||
|
||||
// casper.waitForOpaque('.login-box',
|
||||
// function then() {
|
||||
// this.fillAndSave('#login', falseUser);
|
||||
// },
|
||||
// function onTimeout() {
|
||||
// test.fail('Sign in form didn\'t fade in.');
|
||||
// });
|
||||
|
||||
|
||||
// casper.captureScreenshot('login_spam_test.png');
|
||||
|
||||
// casper.waitForText('attempts remaining!', function then() {
|
||||
// this.fillAndSave('#login', falseUser);
|
||||
// });
|
||||
|
||||
// casper.captureScreenshot('login_spam_test2.png');
|
||||
|
||||
// casper.waitForText('Slow down, there are way too many login attempts!', function onSuccess() {
|
||||
// test.assert(true, 'Spamming the login did result in an error notification');
|
||||
// test.assertSelectorDoesntHaveText('.notification-error', '[object Object]');
|
||||
// }, function onTimeout() {
|
||||
// test.assert(false, 'Spamming the login did not result in an error notification');
|
||||
// });
|
||||
|
||||
// // This test causes the spam notification
|
||||
// // add a wait to ensure future tests don't get tripped up by this.
|
||||
// casper.wait(2000);
|
||||
// }, true);
|
||||
|
||||
CasperTest.begin('Login limit is in place', 4, function suite(test) {
|
||||
CasperTest.Routines.signout.run(test);
|
||||
|
||||
|
@ -93,9 +56,6 @@ CasperTest.begin('Login limit is in place', 4, function suite(test) {
|
|||
}, function onTimeout() {
|
||||
test.assert(false, 'We did not trip the login limit.');
|
||||
});
|
||||
// This test used login, add a wait to
|
||||
// ensure future tests don't get tripped up by this.
|
||||
casper.wait(2000);
|
||||
}, true);
|
||||
|
||||
CasperTest.begin('Can login to Ghost', 5, function suite(test) {
|
||||
|
|
|
@ -16,7 +16,6 @@ describe('DB API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('Notifications API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -17,7 +17,6 @@ describe('Post API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('Settings API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('Slug API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('Tag API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('User API', function () {
|
|||
|
||||
before(function (done) {
|
||||
var app = express();
|
||||
app.set('disableLoginLimiter', true);
|
||||
|
||||
// starting ghost automatically populates the db
|
||||
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
|
||||
|
|
Loading…
Add table
Reference in a new issue