mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 22:11:09 +03:00
Implemented mentions rate limiter (#16223)
closes https://github.com/TryGhost/Team/issues/2419 - adds a rate limiter implementation to the mentions receiving endpoint. - Current configuration is `{"minWait": 10, "maxWait": 100, "lifetime": 1000, "freeRetries": 100}` which is still very open and almost unrestricted. - currently makes use of database storage to track the limits, but can be relatively easily swapped out to something eg Redis should we find this endpoint getting hit too often and maliciously.
This commit is contained in:
parent
6c97edec25
commit
9fc13cfe65
@ -20,7 +20,8 @@ const messages = {
|
||||
error: 'Only {rateSigninAttempts} tries per IP address every {rateSigninPeriod} seconds.',
|
||||
context: 'Too many login attempts.'
|
||||
},
|
||||
tooManyAttempts: 'Too many attempts.'
|
||||
tooManyAttempts: 'Too many attempts.',
|
||||
webmentionsBlock: 'Too many mention attempts'
|
||||
};
|
||||
let spamPrivateBlock = spam.private_block || {};
|
||||
let spamGlobalBlock = spam.global_block || {};
|
||||
@ -29,12 +30,14 @@ let spamUserReset = spam.user_reset || {};
|
||||
let spamUserLogin = spam.user_login || {};
|
||||
let spamMemberLogin = spam.member_login || {};
|
||||
let spamContentApiKey = spam.content_api_key || {};
|
||||
let spamWebmentionsBlock = spam.webmentions_block || {};
|
||||
|
||||
let store;
|
||||
let memoryStore;
|
||||
let privateBlogInstance;
|
||||
let globalResetInstance;
|
||||
let globalBlockInstance;
|
||||
let webmentionsBlockInstance;
|
||||
let userLoginInstance;
|
||||
let membersAuthInstance;
|
||||
let membersAuthEnumerationInstance;
|
||||
@ -123,6 +126,32 @@ const globalReset = () => {
|
||||
return globalResetInstance;
|
||||
};
|
||||
|
||||
const webmentionsBlock = () => {
|
||||
const ExpressBrute = require('express-brute');
|
||||
const BruteKnex = require('brute-knex');
|
||||
const db = require('../../../../data/db');
|
||||
|
||||
store = store || new BruteKnex({
|
||||
tablename: 'brute',
|
||||
createTable: false,
|
||||
knex: db.knex
|
||||
});
|
||||
|
||||
webmentionsBlockInstance = webmentionsBlockInstance || new ExpressBrute(store,
|
||||
extend({
|
||||
attachResetToRequest: false,
|
||||
failCallback(req, res, next) {
|
||||
return next(new errors.TooManyRequestsError({
|
||||
message: messages.webmentionsBlock
|
||||
}));
|
||||
},
|
||||
handleStoreError: handleStoreError
|
||||
}, pick(spamWebmentionsBlock, spamConfigKeys))
|
||||
);
|
||||
|
||||
return webmentionsBlockInstance;
|
||||
};
|
||||
|
||||
const membersAuth = () => {
|
||||
const ExpressBrute = require('express-brute');
|
||||
const BruteKnex = require('brute-knex');
|
||||
@ -319,6 +348,7 @@ module.exports = {
|
||||
userReset: userReset,
|
||||
privateBlog: privateBlog,
|
||||
contentApiKey: contentApiKey,
|
||||
webmentionsBlock: webmentionsBlock,
|
||||
reset: () => {
|
||||
store = undefined;
|
||||
memoryStore = undefined;
|
||||
|
@ -104,5 +104,18 @@ module.exports = {
|
||||
*/
|
||||
membersAuthEnumeration(req, res, next) {
|
||||
return spamPrevention.membersAuthEnumeration().prevent(req, res, next);
|
||||
},
|
||||
|
||||
/**
|
||||
* Blocks webmention spam
|
||||
*/
|
||||
|
||||
webmentionsLimiter(req, res, next) {
|
||||
return spamPrevention.webmentionsBlock().getMiddleware({
|
||||
ignoreIP: false,
|
||||
key(_req, _res, _next) {
|
||||
return _next('webmention_blocked');
|
||||
}
|
||||
})(req, res, next);
|
||||
}
|
||||
};
|
||||
|
@ -11,6 +11,9 @@ module.exports = function apiRoutes() {
|
||||
// shouldn't be cached
|
||||
router.use(shared.middleware.cacheControl('private'));
|
||||
|
||||
// rate limiter
|
||||
router.use(shared.middleware.brute.webmentionsLimiter);
|
||||
|
||||
// Webmentions
|
||||
router.post('/receive', bodyParser.urlencoded({extended: true, limit: '5mb'}), http(api.mentions.receive));
|
||||
|
||||
|
@ -102,6 +102,12 @@
|
||||
"maxWait": 43200000,
|
||||
"lifetime": 43200,
|
||||
"freeRetries": 8
|
||||
},
|
||||
"webmentions_block": {
|
||||
"minWait": 10,
|
||||
"maxWait": 100,
|
||||
"lifetime": 1000,
|
||||
"freeRetries": 100
|
||||
}
|
||||
},
|
||||
"caching": {
|
||||
|
@ -43,6 +43,12 @@
|
||||
"maxWait": 3600000,
|
||||
"lifetime": 3600,
|
||||
"freeRetries":99
|
||||
},
|
||||
"webmentions_block": {
|
||||
"minWait": 10,
|
||||
"maxWait": 100,
|
||||
"lifetime": 1000,
|
||||
"freeRetries": 100
|
||||
}
|
||||
},
|
||||
"privacy": {
|
||||
|
@ -1,4 +1,10 @@
|
||||
const {agentProvider, fixtureManager, mockManager, matchers, sleep} = require('../../utils/e2e-framework');
|
||||
const {
|
||||
agentProvider,
|
||||
fixtureManager,
|
||||
mockManager,
|
||||
dbUtils,
|
||||
configUtils
|
||||
} = require('../../utils/e2e-framework');
|
||||
const models = require('../../../core/server/models');
|
||||
const assert = require('assert');
|
||||
const urlUtils = require('../../../core/shared/url-utils');
|
||||
@ -169,4 +175,41 @@ describe('Webmentions (receiving)', function () {
|
||||
|
||||
emailMockReceiver.sentEmailCount(0);
|
||||
});
|
||||
|
||||
it('is rate limited against spamming mention requests', async function () {
|
||||
await dbUtils.truncate('brute');
|
||||
const webmentionBlock = configUtils.config.get('spam').webmentions_block;
|
||||
const targetUrl = new URL(urlUtils.getSiteUrl());
|
||||
const sourceUrl = new URL('http://testpage.com/external-article-2/');
|
||||
const html = `
|
||||
<html><head><title>Test Page</title><meta name="description" content="Test description"><meta name="author" content="John Doe"></head><body></body></html>
|
||||
`;
|
||||
nock(targetUrl.origin)
|
||||
.head(targetUrl.pathname)
|
||||
.reply(200);
|
||||
|
||||
nock(sourceUrl.origin)
|
||||
.get(sourceUrl.pathname)
|
||||
.reply(200, html, {'Content-Type': 'text/html'});
|
||||
|
||||
// +1 because this is a retry count, so we have one request + the retries, then blocked
|
||||
for (let i = 0; i < webmentionBlock.freeRetries + 1; i++) {
|
||||
await agent.post('/receive/')
|
||||
.body({
|
||||
source: sourceUrl.href,
|
||||
target: targetUrl.href,
|
||||
payload: {}
|
||||
})
|
||||
.expectStatus(202);
|
||||
}
|
||||
|
||||
await agent
|
||||
.post('/receive/')
|
||||
.body({
|
||||
source: sourceUrl.href,
|
||||
target: targetUrl.href,
|
||||
payload: {}
|
||||
})
|
||||
.expectStatus(429);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user