mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Added Captcha service module
ref BAE-369 Added captcha-service module. Currently unused but idea here is that we can add this middleware to forms protected by Captcha to validate the response.
This commit is contained in:
parent
3cf1abfc49
commit
8dd5b0883a
8 changed files with 1449 additions and 13 deletions
6
ghost/captcha-service/.eslintrc.js
Normal file
6
ghost/captcha-service/.eslintrc.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/node'
|
||||
]
|
||||
};
|
23
ghost/captcha-service/README.md
Normal file
23
ghost/captcha-service/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Captcha Service
|
||||
|
||||
Validate CAPTCHAs in Ghost for sign-ups
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
This is a monorepo package.
|
||||
|
||||
Follow the instructions for the top-level repo.
|
||||
1. `git clone` this repo & `cd` into it as usual
|
||||
2. Run `yarn` to install top-level dependencies.
|
||||
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
- `yarn lint` run just eslint
|
||||
- `yarn test` run lint and tests
|
||||
|
1
ghost/captcha-service/index.js
Normal file
1
ghost/captcha-service/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('./lib/CaptchaService');
|
69
ghost/captcha-service/lib/CaptchaService.js
Normal file
69
ghost/captcha-service/lib/CaptchaService.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
const hcaptcha = require('hcaptcha');
|
||||
const logging = require('@tryghost/logging');
|
||||
const {InternalServerError, BadRequestError, utils: errorUtils} = require('@tryghost/errors');
|
||||
|
||||
class CaptchaService {
|
||||
#enabled;
|
||||
#scoreThreshold;
|
||||
#secretKey;
|
||||
|
||||
/**
|
||||
* @param {Object} options
|
||||
* @param {boolean} [options.enabled] Whether hCaptcha is enabled
|
||||
* @param {number} [options.scoreThreshold] Score threshold for bot detection
|
||||
* @param {string} [options.secretKey] hCaptcha secret key
|
||||
*/
|
||||
constructor({
|
||||
enabled,
|
||||
scoreThreshold,
|
||||
secretKey
|
||||
}) {
|
||||
this.#enabled = enabled;
|
||||
this.#secretKey = secretKey;
|
||||
this.#scoreThreshold = scoreThreshold;
|
||||
}
|
||||
|
||||
getMiddleware() {
|
||||
const scoreThreshold = this.#scoreThreshold;
|
||||
const secretKey = this.#secretKey;
|
||||
|
||||
if (!this.#enabled) {
|
||||
return function captchaNoOpMiddleware(req, res, next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
return async function captchaMiddleware(req, res, next) {
|
||||
let captchaResponse;
|
||||
|
||||
try {
|
||||
if (!req.body || !req.body.token) {
|
||||
throw new BadRequestError({
|
||||
message: 'hCaptcha token missing'
|
||||
});
|
||||
}
|
||||
|
||||
captchaResponse = await hcaptcha.verify(secretKey, req.body.token, req.ip);
|
||||
|
||||
if (captchaResponse.score < scoreThreshold) {
|
||||
next();
|
||||
} else {
|
||||
logging.error(`Blocking request due to high score (${captchaResponse.score})`);
|
||||
|
||||
// Intentionally left sparse to avoid leaking information
|
||||
throw new InternalServerError();
|
||||
}
|
||||
} catch (err) {
|
||||
if (errorUtils.isGhostError(err)) {
|
||||
return next(err);
|
||||
} else {
|
||||
return next(new InternalServerError({
|
||||
message: 'Failed to verify hCaptcha token'
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CaptchaService;
|
28
ghost/captcha-service/package.json
Normal file
28
ghost/captcha-service/package.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "@tryghost/captcha-service",
|
||||
"version": "0.0.0",
|
||||
"repository": "https://github.com/TryGhost/Ghost/tree/main/packages/captcha-service",
|
||||
"author": "Ghost Foundation",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "echo \"Implement me!\"",
|
||||
"test:unit": "NODE_ENV=testing c8 --all --check-coverage --100 --reporter text --reporter cobertura mocha './test/**/*.test.js'",
|
||||
"test": "yarn test:unit",
|
||||
"lint:code": "eslint *.js lib/ --ext .js --cache",
|
||||
"lint": "yarn lint:code && yarn lint:test",
|
||||
"lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"c8": "10.1.3",
|
||||
"mocha": "11.0.1",
|
||||
"sinon": "19.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"hcaptcha": "0.2.0"
|
||||
}
|
||||
}
|
6
ghost/captcha-service/test/.eslintrc.js
Normal file
6
ghost/captcha-service/test/.eslintrc.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: ['ghost'],
|
||||
extends: [
|
||||
'plugin:ghost/test'
|
||||
]
|
||||
};
|
124
ghost/captcha-service/test/CaptchaService.test.js
Normal file
124
ghost/captcha-service/test/CaptchaService.test.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
const assert = require('assert/strict');
|
||||
const sinon = require('sinon');
|
||||
const CaptchaService = require('../index');
|
||||
const hcaptcha = require('hcaptcha');
|
||||
|
||||
describe('CaptchaService', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(hcaptcha, 'verify');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
hcaptcha.verify.restore();
|
||||
});
|
||||
|
||||
it('Creates a middleware when enabled', function () {
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: true,
|
||||
secretKey: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
assert.equal(captchaMiddleware.length, 3);
|
||||
});
|
||||
|
||||
it('No-ops if CAPTCHA score is safe', function (done) {
|
||||
hcaptcha.verify.resolves({score: 0.6});
|
||||
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: true,
|
||||
scoreThreshold: 0.8,
|
||||
secretKey: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
|
||||
const req = {
|
||||
body: {
|
||||
token: 'test-token'
|
||||
}
|
||||
};
|
||||
|
||||
captchaMiddleware(req, null, (err) => {
|
||||
assert.equal(err, undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Errors when CAPTCHA score is suspicious', function (done) {
|
||||
hcaptcha.verify.resolves({score: 0.8});
|
||||
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: true,
|
||||
scoreThreshold: 0.8,
|
||||
secretKey: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
|
||||
const req = {
|
||||
body: {
|
||||
token: 'test-token'
|
||||
}
|
||||
};
|
||||
|
||||
captchaMiddleware(req, null, (err) => {
|
||||
assert.equal(err.message, 'The server has encountered an error.');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Fails gracefully if hcaptcha verification fails', function (done) {
|
||||
hcaptcha.verify.rejects(new Error('Test error'));
|
||||
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: true,
|
||||
scoreThreshold: 0.8,
|
||||
secretKey: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
|
||||
const req = {
|
||||
body: {
|
||||
token: 'test-token'
|
||||
}
|
||||
};
|
||||
|
||||
captchaMiddleware(req, null, (err) => {
|
||||
assert.equal(err.message, 'Failed to verify hCaptcha token');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns a 400 if no token provided', function (done) {
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: true,
|
||||
scoreThreshold: 0.8,
|
||||
secret: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
|
||||
const req = {
|
||||
body: {}
|
||||
};
|
||||
|
||||
captchaMiddleware(req, null, (err) => {
|
||||
assert.equal(err.message, 'hCaptcha token missing');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Returns no-op middleware when not enabled', function (done) {
|
||||
const captchaService = new CaptchaService({
|
||||
enabled: false,
|
||||
secretKey: 'test-secret'
|
||||
});
|
||||
|
||||
const captchaMiddleware = captchaService.getMiddleware();
|
||||
captchaMiddleware(null, null, () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue