mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Added honeypot field to prevent bot signup/signin
ref KTLO-1 Should prevent untargeted attacks using headless browser bots.
This commit is contained in:
parent
244e612f53
commit
0a9d2fadba
6 changed files with 90 additions and 6 deletions
|
@ -3,6 +3,9 @@ import {hasMode} from '../../utils/check-mode';
|
|||
import {isCookiesDisabled} from '../../utils/helpers';
|
||||
|
||||
export const InputFieldStyles = `
|
||||
.gh-portal-input-section.hidden {
|
||||
display: none;
|
||||
}
|
||||
.gh-portal-input {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
|
@ -81,6 +84,7 @@ function InputError({message, style}) {
|
|||
function InputField({
|
||||
name,
|
||||
id,
|
||||
hidden,
|
||||
label,
|
||||
hideLabel,
|
||||
type,
|
||||
|
@ -97,6 +101,7 @@ function InputField({
|
|||
}) {
|
||||
const fieldNode = useRef(null);
|
||||
id = id || `input-${name}`;
|
||||
const sectionClasses = hidden ? 'gh-portal-input-section hidden' : 'gh-portal-input-section';
|
||||
const labelClasses = hideLabel ? 'gh-portal-input-label hidden' : 'gh-portal-input-label';
|
||||
const inputClasses = errorMessage ? 'gh-portal-input error' : 'gh-portal-input';
|
||||
if (isCookiesDisabled()) {
|
||||
|
@ -130,7 +135,7 @@ function InputField({
|
|||
}
|
||||
}, [autoFocus]);
|
||||
return (
|
||||
<section className='gh-portal-input-section'>
|
||||
<section className={sectionClasses}>
|
||||
<div className='gh-portal-input-labelcontainer'>
|
||||
<label htmlFor={id} className={labelClasses}> {label} </label>
|
||||
<InputError message={errorMessage} name={name} />
|
||||
|
|
|
@ -12,6 +12,7 @@ const FormInput = ({field, onChange, onBlur = () => { }, onKeyDown = () => {}})
|
|||
label = {field.label}
|
||||
type={field.type}
|
||||
name={field.name}
|
||||
hidden={field.hidden}
|
||||
placeholder={field.placeholder}
|
||||
disabled={field.disabled}
|
||||
value={field.value}
|
||||
|
|
|
@ -34,11 +34,11 @@ export default class SigninPage extends React.Component {
|
|||
errors: ValidateInputForm({fields: this.getInputFields({state})})
|
||||
};
|
||||
}, async () => {
|
||||
const {email, errors} = this.state;
|
||||
const {email, honeypot, errors} = this.state;
|
||||
const {redirect} = this.context.pageData ?? {};
|
||||
const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0);
|
||||
if (!hasFormErrors) {
|
||||
this.context.onAction('signin', {email, redirect});
|
||||
this.context.onAction('signin', {email, honeypot, redirect});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -71,6 +71,18 @@ export default class SigninPage extends React.Component {
|
|||
required: true,
|
||||
errorMessage: errors.email || '',
|
||||
autoFocus: true
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
value: state.honeypot,
|
||||
placeholder: '+1 (123) 456-7890',
|
||||
// Doesn't need translation, hidden field
|
||||
label: 'Phone number',
|
||||
name: 'phonenumber',
|
||||
required: false,
|
||||
tabindex: -1,
|
||||
autocomplete: 'off',
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
return fields;
|
||||
|
|
|
@ -397,7 +397,7 @@ class SignupPage extends React.Component {
|
|||
};
|
||||
}, () => {
|
||||
const {site, onAction} = this.context;
|
||||
const {name, email, plan, errors} = this.state;
|
||||
const {name, email, plan, honeypot, errors} = this.state;
|
||||
const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0);
|
||||
if (!hasFormErrors) {
|
||||
if (hasMultipleNewsletters({site})) {
|
||||
|
@ -410,7 +410,7 @@ class SignupPage extends React.Component {
|
|||
this.setState({
|
||||
errors: {}
|
||||
});
|
||||
onAction('signup', {name, email, plan});
|
||||
onAction('signup', {name, email, honeypot, plan});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -484,6 +484,18 @@ class SignupPage extends React.Component {
|
|||
required: true,
|
||||
tabindex: 2,
|
||||
errorMessage: errors.email || ''
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
value: state.honeypot,
|
||||
placeholder: '+1 (123) 456-7890',
|
||||
// Doesn't need translation, hidden field
|
||||
label: 'Phone number',
|
||||
name: 'phonenumber',
|
||||
required: false,
|
||||
tabindex: -1,
|
||||
autocomplete: 'off',
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -470,7 +470,7 @@ module.exports = class RouterController {
|
|||
}
|
||||
|
||||
async sendMagicLink(req, res) {
|
||||
const {email, autoRedirect} = req.body;
|
||||
const {email, honeypot, autoRedirect} = req.body;
|
||||
let {emailType, redirect} = req.body;
|
||||
|
||||
let referer = req.get('referer');
|
||||
|
@ -492,6 +492,13 @@ module.exports = class RouterController {
|
|||
});
|
||||
}
|
||||
|
||||
if (honeypot) {
|
||||
// Honeypot field is filled, this is a bot.
|
||||
// Pretend that the email was sent successfully.
|
||||
res.writeHead(201);
|
||||
return res.end('Created.');
|
||||
}
|
||||
|
||||
if (!emailType) {
|
||||
// Default to subscribe form that also allows to login (safe fallback for older clients)
|
||||
if (!this._allowSelfSignup()) {
|
||||
|
|
|
@ -388,5 +388,52 @@ describe('RouterController', function () {
|
|||
await controller.sendMagicLink(req, res).should.be.rejectedWith(`Cannot subscribe to archived newsletters Newsletter 2`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('honeypot', function () {
|
||||
let req, res, sendEmailWithMagicLinkStub;
|
||||
|
||||
const createRouterController = (deps = {}) => {
|
||||
return new RouterController({
|
||||
allowSelfSignup: sinon.stub().returns(true),
|
||||
memberAttributionService: {
|
||||
getAttribution: sinon.stub().resolves({})
|
||||
},
|
||||
sendEmailWithMagicLink: sendEmailWithMagicLinkStub,
|
||||
...deps
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
req = {
|
||||
body: {
|
||||
email: 'jamie@example.com',
|
||||
emailType: 'signup'
|
||||
},
|
||||
get: sinon.stub()
|
||||
};
|
||||
res = {
|
||||
writeHead: sinon.stub(),
|
||||
|
||||
end: sinon.stub()
|
||||
};
|
||||
sendEmailWithMagicLinkStub = sinon.stub().resolves();
|
||||
});
|
||||
|
||||
it('Sends emails when honeypot is not filled', async function () {
|
||||
const controller = createRouterController();
|
||||
|
||||
req.body.honeypot = 'filled!';
|
||||
|
||||
await controller.sendMagicLink(req, res).should.be.fulfilled();
|
||||
sendEmailWithMagicLinkStub.notCalled.should.be.true();
|
||||
});
|
||||
|
||||
it('Does not send emails when honeypot is filled', async function () {
|
||||
const controller = createRouterController();
|
||||
|
||||
await controller.sendMagicLink(req, res).should.be.fulfilled();
|
||||
sendEmailWithMagicLinkStub.calledOnce.should.be.true();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue