fix(antispam): harden spam guard with signed form tokens and proxy-safe IP extraction
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 51s
Build & Deploy / 🔔 Notify (push) Successful in 2s
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 19s
Build & Deploy / 🧪 QA (push) Successful in 1m17s
Build & Deploy / 🏗️ Build (push) Successful in 2m33s
Build & Deploy / 🚀 Deploy (push) Successful in 25s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 51s
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Rate limit now uses the last X-Forwarded-For hop (appended by Traefik) instead of the client-controlled first hop, which bots rotated freely - Time-trap anchored to server time via HMAC-signed form token (FORM_TOKEN_SECRET) instead of client-supplied form_loaded_at - Fix RequestQuoteForm regression: FormData was built from scratch without honeypot/token hidden inputs and inputs lacked name attributes, so every legitimate quote request was silently blocked as spam
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button, Heading, Card, Input, Textarea, Label } from '@/components/ui';
|
||||
import { sendContactFormAction } from '@/app/actions/contact';
|
||||
import { sendContactFormAction, issueFormTokenAction } from '@/app/actions/contact';
|
||||
import { useAnalytics } from '@/components/analytics/useAnalytics';
|
||||
import { AnalyticsEvents } from '@/components/analytics/analytics-events';
|
||||
|
||||
@@ -12,7 +12,18 @@ export default function ContactForm() {
|
||||
const { trackEvent } = useAnalytics();
|
||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle');
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const [formLoadedAt] = useState(() => Date.now());
|
||||
const [formToken, setFormToken] = useState<string | null>(null);
|
||||
|
||||
// Server-signed anti-spam token: proves the submission comes from a real page load
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
issueFormTokenAction().then((token) => {
|
||||
if (!cancelled) setFormToken(token);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFocus = (fieldId: string) => {
|
||||
// Initial form start
|
||||
@@ -153,8 +164,8 @@ export default function ContactForm() {
|
||||
style={{ display: 'none' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* Anti-spam time-trap: server rejects submissions faster than a human could fill the form */}
|
||||
<input type="hidden" name="form_loaded_at" value={formLoadedAt} />
|
||||
{/* Anti-spam time-trap anchor: server-signed token issued at page load */}
|
||||
<input type="hidden" name="form_token" value={formToken ?? ''} />
|
||||
<div className="space-y-1 md:space-y-2">
|
||||
<Label htmlFor="contact-name">{t('form.name')}</Label>
|
||||
<Input
|
||||
|
||||
Reference in New Issue
Block a user