diff --git a/src/pages/Contact.module.css b/src/pages/Contact.module.css index 80dfa15..ff312db 100644 --- a/src/pages/Contact.module.css +++ b/src/pages/Contact.module.css @@ -139,3 +139,17 @@ .infoItem a:hover { 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; +} diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index 6332455..36c4096 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -35,6 +35,7 @@ export function Contact() { subject: "", message: "", }); + const [honeypot, setHoneypot] = useState(""); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [submitStatus, setSubmitStatus] = useState< @@ -79,6 +80,15 @@ export function Contact() { const handleSubmit = async (e: FormEvent) => { 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); if (!validateForm()) return; @@ -166,6 +176,19 @@ export function Contact() { className={styles.form} noValidate > + {/* Honeypot field - invisible to users, tempting to bots */} + setHoneypot(e.target.value)} + className={styles.honeypot} + tabIndex={-1} + autoComplete="off" + aria-hidden="true" + data-testid="honeypot-input" + /> + { render(); // Fill out the form - fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); - fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); - fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Test Subject' } }); - fireEvent.change(screen.getByLabelText('Message'), { target: { value: 'Hello world' } }); + 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' } }); // Submit fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); @@ -124,10 +124,10 @@ describe('Contact Page', () => { render(); // Fill out with malicious input - fireEvent.change(screen.getByLabelText('Name'), { target: { value: '' } }); - fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'john@example.com' } }); - fireEvent.change(screen.getByLabelText('Subject'), { target: { value: 'Bold' } }); - fireEvent.change(screen.getByLabelText('Message'), { target: { value: '"Quotes"' } }); + fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: '' } }); + fireEvent.change(screen.getByPlaceholderText('Your Email'), { target: { value: 'john@example.com' } }); + fireEvent.change(screen.getByPlaceholderText('Message Subject'), { target: { value: 'Bold' } }); + fireEvent.change(screen.getByPlaceholderText('Your Message'), { target: { value: '"Quotes"' } }); // Submit fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); @@ -161,7 +161,7 @@ describe('Contact Page', () => { const longName = 'a'.repeat(101); // Fill out the form - fireEvent.change(screen.getByLabelText('Name'), { target: { value: longName } }); + fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: longName } }); // Submit fireEvent.click(screen.getByRole('button', { name: 'Send Message' })); @@ -178,10 +178,10 @@ describe('Contact Page', () => { const { container } = render(); // Fill out the form with invalid email (XSS vector) - fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'John Doe' } }); - fireEvent.change(screen.getByLabelText('Email'), { target: { value: '