0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-18 02:21:47 -05:00

Use Captcha middleware in members API

ref BAE-104

The members send-magic-link API should be protected by Captcha. This
required initialising the Captcha service in the members API, and
putting the middleware into the send-magic-link API.

If it's enabled via lab flag and config, then the service will prevent
API calls that don't have a valid Captcha response.
This commit is contained in:
Sam Lord 2025-01-27 15:57:31 +00:00 committed by GitHub
parent 3e2658baa0
commit 439bbf8b79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 161 additions and 1 deletions

View file

@ -1,6 +1,19 @@
const settingsCache = require('../../../shared/settings-cache');
const urlUtils = require('../../../shared/url-utils');
const ghostVersion = require('@tryghost/version');
const config = require('../../../shared/config');
const labs = require('../../../shared/labs');
const getCaptchaSettings = () => {
if (labs.isSet('captcha')) {
return {
captcha_enabled: config.get('captcha:enabled'),
captcha_sitekey: config.get('captcha:siteKey')
};
} else {
return {};
}
};
/** @type {import('@tryghost/api-framework').Controller} */
const controller = {
@ -18,7 +31,8 @@ const controller = {
settingsCache.getPublic(), {
url: urlUtils.urlFor('home', true),
version: ghostVersion.safe
}
},
getCaptchaSettings()
);
}
}

View file

@ -18,6 +18,7 @@ const tiersService = require('../tiers');
const newslettersService = require('../newsletters');
const memberAttributionService = require('../member-attribution');
const emailSuppressionList = require('../email-suppression-list');
const CaptchaService = require('@tryghost/captcha-service');
const {t} = require('../i18n');
const sentry = require('../../../shared/sentry');
const sharedConfig = require('../../../shared/config');
@ -240,6 +241,11 @@ function createApiInstance(config) {
settingsCache,
sentry,
settingsHelpers,
captchaService: new CaptchaService({
enabled: labsService.isSet('captcha') && sharedConfig.get('captcha:enabled'),
scoreThreshold: sharedConfig.get('captcha:scoreThreshold'),
secretKey: sharedConfig.get('captcha:secretKey')
}),
config: sharedConfig
});

View file

@ -71,6 +71,7 @@
"@tryghost/audience-feedback": "0.0.0",
"@tryghost/bookshelf-plugins": "0.6.25",
"@tryghost/bootstrap-socket": "0.0.0",
"@tryghost/captcha-service": "0.0.0",
"@tryghost/color-utils": "0.2.2",
"@tryghost/config-url-helpers": "1.0.12",
"@tryghost/constants": "0.0.0",

View file

@ -104,3 +104,110 @@ Object {
"x-powered-by": "Express",
}
`;
exports[`Settings Content API Captcha settings Can request captcha settings 1: [body] 1`] = `
Object {
"meta": Object {},
"settings": Object {
"accent_color": "#FF1A75",
"allow_self_signup": true,
"captcha_enabled": true,
"captcha_sitekey": "testkey",
"codeinjection_foot": null,
"codeinjection_head": null,
"comments_enabled": "off",
"cover_image": "https://static.ghost.org/v5.0.0/images/publication-cover.jpg",
"default_email_address": "noreply@127.0.0.1",
"description": "Thoughts, stories and ideas",
"editor_default_email_recipients": "visibility",
"facebook": "ghost",
"firstpromoter_account": null,
"icon": null,
"labs": Any<Object>,
"lang": "en",
"locale": "en",
"logo": null,
"members_enabled": true,
"members_invite_only": false,
"members_signup_access": "all",
"members_support_address": "noreply",
"meta_description": null,
"meta_title": null,
"navigation": Array [
Object {
"label": "Home",
"url": "/",
},
Object {
"label": "About",
"url": "/about/",
},
Object {
"label": "Collection",
"url": "/tag/getting-started/",
},
Object {
"label": "Author",
"url": "/author/ghost/",
},
Object {
"label": "Portal",
"url": "/portal/",
},
],
"og_description": null,
"og_image": null,
"og_title": null,
"outbound_link_tagging": true,
"paid_members_enabled": true,
"portal_button": true,
"portal_button_icon": null,
"portal_button_signup_text": "Subscribe",
"portal_button_style": "icon-and-text",
"portal_default_plan": "yearly",
"portal_name": true,
"portal_plans": Array [
"free",
],
"portal_signup_checkbox_required": false,
"portal_signup_terms_html": null,
"recommendations_enabled": false,
"secondary_navigation": Array [
Object {
"label": "Data & privacy",
"url": "/privacy/",
},
Object {
"label": "Contact",
"url": "/contact/",
},
Object {
"label": "Contribute →",
"url": "/contribute/",
},
],
"support_email_address": "noreply@127.0.0.1",
"timezone": "Etc/UTC",
"title": "Ghost",
"twitter": "@ghost",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"url": "http://127.0.0.1:2369/",
"version": Any<String>,
},
}
`;
exports[`Settings Content API Captcha settings Can request captcha settings 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "*",
"cache-control": "public, max-age=0",
"content-length": StringMatching /\\\\d\\+/,
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Accept-Encoding",
"x-powered-by": "Express",
}
`;

View file

@ -1,5 +1,6 @@
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyContentLength, anyContentVersion} = matchers;
const configUtils = require('../../utils/configUtils');
const settingsMatcher = {
version: matchers.anyString,
@ -27,4 +28,33 @@ describe('Settings Content API', function () {
settings: settingsMatcher
});
});
describe('Captcha settings', function () {
beforeEach(function () {
configUtils.set('captcha', {
enabled: true,
siteKey: 'testkey'
});
});
afterEach(function () {
configUtils.restore();
});
it('Can request captcha settings', async function () {
await agent.get('settings/')
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag,
'content-version': anyContentVersion,
'content-length': anyContentLength
})
.matchBodySnapshot({
settings: Object.assign({}, settingsMatcher, {
captcha_enabled: true,
captcha_sitekey: 'testkey'
})
});
});
});
});

View file

@ -74,6 +74,7 @@ module.exports = function MembersAPI({
settingsCache,
sentry,
settingsHelpers,
captchaService,
config
}) {
const tokenService = new TokenService({
@ -337,6 +338,7 @@ module.exports = function MembersAPI({
const middleware = {
sendMagicLink: Router().use(
body.json(),
captchaService.getMiddleware(),
forwardError((req, res) => routerController.sendMagicLink(req, res))
),
createCheckoutSession: Router().use(