Guida pratica per proteggere le registrazioni SaaS B2B
Il mese scorso, un cliente SaaS B2B si e rivolto a noi con un problema familiare: le registrazioni sembravano normali in superficie, ma i crediti gratuiti sparivano rapidamente. Analizzando i log abbiamo scoperto un attacco coordinato — decine di registrazioni dagli stessi subnet /24, email usa e getta in rotazione e un attaccante che riutilizzava infrastrutture su piu account falsi.
Questo articolo illustra la stack di difesa che abbiamo raccomandato. Nomi e identificatori sono anonimizzati, ma il flusso e pronto per la produzione.
Cosa ha facilitato l'attacco
Tre fattori hanno giocato a favore dell'attaccante:
- Tutti i crediti gratuiti concessi alla registrazione — nessuna frizione, nessuna verifica, valore immediato.
- Nessun legame al dispositivo — la stessa impronta del browser poteva registrarsi ripetutamente.
- Controlli IP assenti o mal configurati — dietro il proxy Cloudflare (nuvola arancione), l'app leggeva IP proxy invece dell'indirizzo reale del visitatore.
Proteggere le registrazioni non e una soluzione unica, ma stratificare segnali che si rafforzano a vicenda.
Priorita 1: leggere l'IP reale
Se la tua app e dietro Cloudflare con proxy attivo (nuvola arancione), req.ip o X-Forwarded-For da soli non bastano. Cloudflare invia l'IP reale del visitatore nell'header CF-Connecting-IP.
function getClientIp(request: Request): string {
const cfIp = request.headers.get("cf-connecting-ip");
if (cfIp) return cfIp;
const forwarded = request.headers.get("x-forwarded-for");
if (forwarded) return forwarded.split(",")[0].trim();
return "0.0.0.0";
}
Ogni controllo successivo — rate limit, reputazione IP, blocco — dipende dal fare correttamente questo primo passo.
Priorita 2: valutare l'IP alla registrazione
Esegui la API Veille IP Reputation in modo asincrono durante la registrazione. Se possibile, non bloccare la risposta HTTP su di essa; accoda il controllo e agisci entro secondi.
const API_KEY = process.env.VEILLE_API_KEY!;
const BASE_URL = "https://api.veille.io/v1";
async function scoreIp(ip: string) {
const response = await fetch(
`${BASE_URL}/intelligence/ip?query=${encodeURIComponent(ip)}`,
{ headers: { "x-api-key": API_KEY } }
);
return response.json();
}
Il threat_score aggrega i segnali rilevati:
| Score | Significato |
|---|---|
| 0 | IP residenziale o aziendale pulita |
| 1–25 | Segnali minori (datacenter, VPN) |
| 25–50 | Rischio moderato (segnali multipli) |
| 50+ | Rischio alto (Tor, abusatore noto, proxy) — bloccare o richiedere verifica extra |
Nei log del nostro cliente, le IP attaccanti si raggruppavano in range /24 con score superiori a 50. Bloccare la subnet dopo abusi ripetuti, non solo la singola IP, ha ridotto rapidamente il volume.
Priorita 3: integrare FingerprintJS
Installa FingerprintJS sulle pagine di registrazione e login. Memorizza il visitorId ad ogni tentativo.
import FingerprintJS from "@fingerprintjs/fingerprintjs";
async function getFingerprint(): Promise<string> {
const fp = await FingerprintJS.load();
const result = await fp.get();
return result.visitorId;
}
Poi applica rate limit su IP e impronta digitale:
- Dopo 5 tentativi falliti di registrazione o login dallo stesso fingerprint o IP → blocco per 24 ore.
- Registra entrambi i valori ad ogni tentativo per pivotare facilmente nel back office.
Priorita 4: validare l'email in modo asincrono
Le email usa e getta non fermeranno un attaccante determinato, ma filtrano gran parte dell'abuso a basso sforzo. Chiama la API Veille Email Validation in parallelo al controllo IP:
async function validateEmail(email: string) {
const response = await fetch(
`${BASE_URL}/intelligence/email?query=${encodeURIComponent(email)}`,
{ headers: { "x-api-key": API_KEY } }
);
return response.json();
}
// Nel handler di registrazione:
const [ipResult, emailResult] = await Promise.all([
scoreIp(clientIp),
validateEmail(email),
]);
if (emailResult.disposable || emailResult.risk_score > 70) {
await blockIpTemporarily(clientIp, "disposable_email");
return { error: "Registrazione bloccata" };
}
if (ipResult.threat_score >= 50) {
await blockIpTemporarily(clientIp, "high_threat_ip");
return { error: "Registrazione bloccata" };
}
Esegui questi controlli dopo aver ricevuto il formulario ma prima di concedere crediti. Se un segnale fallisce, blocca l'IP per una finestra configurabile e restituisci un errore generico.
Priorita 5: collegare l'impronta alla verifica email
Creare un account non equivale a provare di controllare la casella. Richiedi la verifica email prima che i crediti siano utilizzabili.
Il passo critico che la maggior parte dei team salta: l'impronta alla registrazione deve corrispondere all'impronta al clic sul link di verifica.
// Alla registrazione — memorizzare verifica in sospeso
await db.pendingVerifications.create({
email,
fingerprintSignup: fingerprint,
ipSignup: clientIp,
token: verificationToken,
expiresAt: addHours(new Date(), 24),
});
// Alla verifica — confrontare impronta
const pending = await db.pendingVerifications.findByToken(token);
if (pending.fingerprintSignup !== currentFingerprint) {
await flagAccount(pending.email, "fingerprint_mismatch");
return { error: "Verifica fallita" };
}
await activateAccount(pending.email);
Una discrepanza non significa sempre frode — gli utenti cambiano dispositivo — ma discrepanze ripetute da IP ad alto rischio sono un segnale forte.
Priorita 6: smettere di regalare tutti i crediti alla registrazione
I crediti gratuiti sono il ROI dell'attaccante. Distribuisci valore su azioni difficili da automatizzare:
| Milestone | Crediti | Perche |
|---|---|---|
| Account creato | 0 | Nessun valore immediato da estrarre |
| Email verificata | +10 | Dimostra controllo della casella |
| Onboarding completato | +25 | Richiede interazione reale col prodotto |
| Condivisione social confermata | +15 | Aggiunge frizione e traccia di audit |
Esempio di flag nel back office:
interface UserCredits {
emailVerified: boolean; // +10 crediti
onboardingCompleted: boolean; // +25 crediti
sharedOnSocial: boolean; // +15 crediti
}
function getAvailableCredits(user: UserCredits): number {
let credits = 0;
if (user.emailVerified) credits += 10;
if (user.onboardingCompleted) credits += 25;
if (user.sharedOnSocial) credits += 15;
return credits;
}
I bot ottimizzano il percorso piu veloce verso i crediti. Un onboarding con chiamate API reali, scelte di configurazione o un breve tutorial interrompe i flussi scriptati.
Panoramica del flusso di registrazione
L'utente invia il formulario
│
▼
Leggere CF-Connecting-IP
│
▼
Verificare rate limit impronta + IP ──► bloccato? → 429, log
│
▼
Creare account (0 crediti)
│
├──► async: reputazione IP
└──► async: validazione email
│
▼
minaccia rilevata? → bloccare IP, segnalare account
│
▼
Inviare email di verifica (memorizzare impronta registrazione)
│
▼
Clic sul link → impronta corrisponde? → +10 crediti
│
▼
Onboarding completato → +25 crediti
│
▼
Condivisione social confermata → +15 crediti
Boolean utili nel back office
Dai al tuo team toggle per reagire senza deploy:
block_disposable_emails— rifiutare caselle usa e getta alla registrazioneblock_high_threat_ips— bloccare auto IP conthreat_score >= 50require_fingerprint_match— imporre corrispondenza impronta nella verifica emailrate_limit_by_fingerprint— attivare throttling per improntagrant_credits_on_signup— mantenere afalsedurante un attacco attivo
Questi flag permettono di rafforzare le difese durante un incidente e allentarle quando il traffico si normalizza.
Ultima risorsa: rallentare senza avvisare
Se un IP e chiaramente abusivo ma vuoi evitare di allertare l'attaccante, alcuni team introducono frizione artificiale — risposte ritardate, CAPTCHA al secondo tentativo o un codice SMS falso che non arriva mai. E controverso, va riservato come ultima risorsa, chiaramente registrato internamente e mai mostrato agli utenti legittimi.
L'obiettivo e bruciare il tempo dell'attaccante, non il tuo.
Risultati
Dopo l'implementazione di questa stack — estrazione IP reale, controlli Veille IP ed email, rate limit FingerprintJS, verifica legata all'impronta e crediti progressivi — il volume di registrazioni abusive del cliente e calato nettamente entro 48 ore. Gli utenti legittimi hanno completato l'onboarding senza notare i controlli aggiuntivi.