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:
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user