feat(security): add honeypot field to contact form

Added a hidden input field (honeypot) to the Contact form to detect and block automated bot submissions.
This enhancement improves availability and integrity by reducing spam without affecting legitimate users.
Also updated Contact.test.tsx to include a test case for the honeypot and improve selector robustness.

Co-authored-by: ragusa-it <196988693+ragusa-it@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-02-08 02:09:11 +00:00
parent 2587b9dd29
commit 10b0480a6f
3 changed files with 81 additions and 17 deletions

View File

@@ -139,3 +139,17 @@
.infoItem a:hover { .infoItem a:hover {
text-decoration: underline; text-decoration: underline;
} }
.honeypot {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
opacity: 0;
pointer-events: none;
}

View File

@@ -35,6 +35,7 @@ export function Contact() {
subject: "", subject: "",
message: "", message: "",
}); });
const [honeypot, setHoneypot] = useState("");
const [errors, setErrors] = useState<FormErrors>({}); const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [submitStatus, setSubmitStatus] = useState< const [submitStatus, setSubmitStatus] = useState<
@@ -79,6 +80,15 @@ export function Contact() {
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
// Honeypot check: If the hidden field is filled, it's a bot.
// We silently "succeed" to fool the bot and protect our resources.
if (honeypot) {
setSubmitStatus("success");
setFormData({ name: "", email: "", subject: "", message: "" });
setHoneypot("");
return;
}
setRateLimitError(false); setRateLimitError(false);
if (!validateForm()) return; if (!validateForm()) return;
@@ -166,6 +176,19 @@ export function Contact() {
className={styles.form} className={styles.form}
noValidate noValidate
> >
{/* Honeypot field - invisible to users, tempting to bots */}
<input
type="text"
name="website"
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
className={styles.honeypot}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
data-testid="honeypot-input"
/>
<Input <Input
label={t.contact.form.name} label={t.contact.form.name}
required required

View File

@@ -70,10 +70,10 @@ describe('Contact Page', () => {
render(<Contact />); render(<Contact />);
// Fill out the form // Fill out the form
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: 'John Doe' } });
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: 'john@example.com' } });
fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Test Subject' } }); fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: 'Test Subject' } });
fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'Hello world' } }); fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: 'Hello world' } });
// Submit // Submit
fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); fireEvent.click(screen.getByRole('button', { name: 'Send Message' }));
@@ -124,10 +124,10 @@ describe('Contact Page', () => {
render(<Contact />); render(<Contact />);
// Fill out with malicious input // Fill out with malicious input
fireEvent.change(screen.getByLabelText('Name'), { target: { value: '<script>alert(1)</script>' } }); fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: '<script>alert(1)</script>' } });
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: 'john@example.com' } });
fireEvent.change(screen.getByLabelText('Subject'), { target: { value: '<b>Bold</b>' } }); fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: '<b>Bold</b>' } });
fireEvent.change(screen.getByLabelText('Message'), { target: { value: '"Quotes"' } }); fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: '"Quotes"' } });
// Submit // Submit
fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); fireEvent.click(screen.getByRole('button', { name: 'Send Message' }));
@@ -161,7 +161,7 @@ describe('Contact Page', () => {
const longName = 'a'.repeat(101); const longName = 'a'.repeat(101);
// Fill out the form // Fill out the form
fireEvent.change(screen.getByLabelText('Name'), { target: { value: longName } }); fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: longName } });
// Submit // Submit
fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); fireEvent.click(screen.getByRole('button', { name: 'Send Message' }));
@@ -178,10 +178,10 @@ describe('Contact Page', () => {
const { container } = render(<Contact />); const { container } = render(<Contact />);
// Fill out the form with invalid email (XSS vector) // Fill out the form with invalid email (XSS vector)
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: 'John Doe' } });
fireEvent.change(screen.getByLabelText('Email'), { target: { value: '<script>@example.com' } }); fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: '<script>@example.com' } });
fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Test Subject' } }); fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: 'Test Subject' } });
fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'Hello world' } }); fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: 'Hello world' } });
// Submit via form submit event to bypass browser validation (jsdom/browser would block this otherwise) // Submit via form submit event to bypass browser validation (jsdom/browser would block this otherwise)
// This ensures our application-level validation logic (isValidEmail) is tested // This ensures our application-level validation logic (isValidEmail) is tested
@@ -204,10 +204,10 @@ describe('Contact Page', () => {
render(<Contact />); render(<Contact />);
// Fill out the form // Fill out the form
fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: 'John Doe' } });
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: 'john@example.com' } });
fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Test Subject' } }); fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: 'Test Subject' } });
fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'Hello world' } }); fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: 'Hello world' } });
// Submit // Submit
fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); fireEvent.click(screen.getByRole('button', { name: 'Send Message' }));
@@ -223,4 +223,31 @@ describe('Contact Page', () => {
expect(errorMessage.getAttribute('role')).toBe('alert'); expect(errorMessage.getAttribute('role')).toBe('alert');
expect(errorMessage.getAttribute('aria-live')).toBe('polite'); expect(errorMessage.getAttribute('aria-live')).toBe('polite');
}); });
it('silently blocks submission if honeypot field is filled (bot detection)', async () => {
render(<Contact />);
// Fill out the form correctly
fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: 'John Doe' } });
fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: 'john@example.com' } });
fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: 'Test Subject' } });
fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: 'Hello world' } });
// Fill the honeypot field
const honeypot = screen.getByTestId('honeypot-input');
fireEvent.change(honeypot, { target: { value: 'I am a bot' } });
// Submit
fireEvent.click(screen.getByRole('button', { name: 'Send Message' }));
// Wait for potential async actions
await waitFor(() => {
// EmailJS should NOT be called
expect(emailjs.send).not.toHaveBeenCalled();
});
// The form should show success to fool the bot
const successMessage = await screen.findByText('Message sent successfully!');
expect(successMessage).toBeTruthy();
});
}); });