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

- 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:
2026-09-02 10:06:07 +02:00
parent 6b3f9b9b25
commit 99467f1379
10 changed files with 278 additions and 38 deletions

View File

@@ -4,6 +4,16 @@ import { sendEmail } from '@/lib/mail/mailer';
import { render, ContactFormNotification, ConfirmationMessage } from '@mintel/mail';
import React from 'react';
import { getServerAppServices } from '@/lib/services/create-services.server';
import { createFormToken } from '@/lib/antispam/form-token';
import { parseClientIp } from '@/lib/antispam/client-ip';
/**
* Issues a server-signed form token used by the anti-spam guard to prove the
* submission originates from a real page load (server-anchored time-trap).
*/
export async function issueFormTokenAction(): Promise<string> {
return createFormToken(Date.now());
}
export async function sendContactFormAction(formData: FormData) {
const services = getServerAppServices();
@@ -25,13 +35,13 @@ export async function sendContactFormAction(formData: FormData) {
// Track attempt
services.analytics.track('contact-form-attempt');
// Anti-spam guard: honeypot, time-trap, email validation, link limit, IP rate limit
// Anti-spam guard: honeypot, signed form token (server-anchored time-trap), email validation, link limit, IP rate limit
const { checkContactSubmission } = await import('@/lib/antispam/contact-guard');
const verdict = checkContactSubmission({
honeypot: (formData.get('company_website') as string) || null,
formLoadedAt: Number(formData.get('form_loaded_at')) || null,
formToken: (formData.get('form_token') as string) || null,
now: Date.now(),
ip: requestHeaders.get('x-forwarded-for')?.split(',')[0]?.trim() || null,
ip: parseClientIp(requestHeaders.get('x-forwarded-for')),
email: (formData.get('email') as string) || null,
message: (formData.get('message') as string) || null,
});