Skip to content

Commit e0fe6c3

Browse files
authored
chore: honeypot to fight contact spam (#915)
1 parent 1ed4bf6 commit e0fe6c3

File tree

1 file changed

+41
-3
lines changed

1 file changed

+41
-3
lines changed

src/components/shared/contact-form/contact-form.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type ValueType = {
2828
email: string;
2929
company: string;
3030
message?: string;
31+
website?: string; // Honeypot field
3132
};
3233

3334
const validationSchema = yup.object().shape({
@@ -40,6 +41,7 @@ const validationSchema = yup.object().shape({
4041
.required('Work email is a required field'),
4142
company: yup.string().trim().required('Company name is a required field'),
4243
message: yup.string().trim().optional(),
44+
website: yup.string().trim().optional(), // Honeypot field - should be empty
4345
});
4446

4547
const getButtonTitle = (formId: string) => {
@@ -131,7 +133,15 @@ const detectSpamSubmission = (values: ValueType): boolean => {
131133
if (company.trim().length <= 2) spamScore += 3;
132134

133135
// Medium confidence indicators (2 points each)
134-
const freeEmailDomains = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'aol.com', 'icloud.com', 't-online.de'];
136+
const freeEmailDomains = [
137+
'gmail.com',
138+
'yahoo.com',
139+
'hotmail.com',
140+
'outlook.com',
141+
'aol.com',
142+
'icloud.com',
143+
't-online.de',
144+
];
135145
const emailDomain = email.toLowerCase().split('@')[1] || '';
136146
if (!emailDomain) {
137147
// Invalid email format (missing domain) - likely spam
@@ -141,7 +151,10 @@ const detectSpamSubmission = (values: ValueType): boolean => {
141151
}
142152

143153
// Low confidence indicators (1 point each)
144-
const messageWords = (message || '').trim().split(/\s+/).filter(word => word.length > 0);
154+
const messageWords = (message || '')
155+
.trim()
156+
.split(/\s+/)
157+
.filter((word) => word.length > 0);
145158
if (messageWords.length < 5) spamScore += 1;
146159

147160
// Flag as spam if score >= 5
@@ -176,7 +189,18 @@ const ContactForm = ({
176189
} = useForm<ValueType>({ resolver: yupResolver(validationSchema) });
177190

178191
const onSubmit = async (values: ValueType) => {
179-
const { firstname, lastname, email, company, message } = values;
192+
const { firstname, lastname, email, company, message, website } = values;
193+
194+
// Honeypot check - if filled, it's a bot
195+
if (website?.trim()) {
196+
// Silently reject the submission (don't show error to bot)
197+
setButtonState(STATES.SUCCESS);
198+
setTimeout(() => {
199+
setButtonState(STATES.DEFAULT);
200+
reset();
201+
}, BUTTON_SUCCESS_TIMEOUT_MS);
202+
return;
203+
}
180204

181205
setButtonState(STATES.LOADING);
182206
setFormError('');
@@ -313,6 +337,20 @@ const ContactForm = ({
313337
error={errors?.message?.message}
314338
{...register('message')}
315339
/>
340+
{/* Honeypot field - hidden from users but visible to bots */}
341+
<div className="pointer-events-none absolute left-[-9999px] opacity-0" aria-hidden="true">
342+
<label htmlFor="website-field" tabIndex={-1}>
343+
Website
344+
</label>
345+
<Field
346+
id="website-field"
347+
type="text"
348+
placeholder="https://yourcompany.com"
349+
autoComplete="off"
350+
tabIndex={-1}
351+
{...register('website')}
352+
/>
353+
</div>
316354
<div className="relative col-span-full flex items-center gap-x-5 sm:flex-col sm:items-start sm:gap-y-2">
317355
<div className="flex w-full max-w-[260px] flex-col items-center lg:max-w-[320px] sm:max-w-full">
318356
<Button

0 commit comments

Comments
 (0)