Retour au blog
Sécurité

Guide pratique pour sécuriser les inscriptions SaaS B2B

Josselin Liebe
Josselin Liebe

Le mois dernier, un client SaaS B2B est venu nous voir avec un problème classique : les inscriptions semblaient normales en surface, mais les crédits gratuits partaient vite. En analysant les logs, on a identifié une attaque coordonnée — des dizaines d'inscriptions depuis les mêmes sous-réseaux /24, des emails jetables en rotation, et un attaquant réutilisant la même infrastructure sur plusieurs faux comptes.

Cet article détaille la stack de défense qu'on leur a recommandée. Les noms et identifiants sont anonymisés, mais le flux est prêt pour la production.

Ce qui facilitait l'attaque

Trois facteurs jouaient en faveur de l'attaquant :

  1. Tous les crédits gratuits accordés à l'inscription — aucune friction, aucune vérification, valeur immédiate.
  2. Aucun lien device — le même fingerprint navigateur pouvait s'inscrire encore et encore.
  3. Vérification IP absente ou mal configurée — derrière le proxy Cloudflare (nuage orange), l'app lisait les IPs du proxy au lieu de l'adresse réelle du visiteur.

Sécuriser les inscriptions, ce n'est pas une solution miracle unique. C'est empiler des signaux qui se renforcent mutuellement.

Priorité 1 : lire la vraie adresse IP

Si votre app est derrière Cloudflare avec le proxy activé (nuage orange), req.ip ou X-Forwarded-For seul ne suffit pas. Cloudflare envoie l'IP réelle du visiteur dans l'en-tête 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";
}

Chaque contrôle en aval — rate limits, réputation IP, blocage — dépend de cette première étape.

Priorité 2 : scorer l'IP à l'inscription

Lancez l'API Veille IP Reputation de manière asynchrone pendant l'inscription. Si possible, ne bloquez pas la réponse HTTP dessus ; mettez le check en file et agissez en quelques secondes.

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();
}

Le threat_score agrège les signaux détectés :

Score Signification
0 IP résidentielle ou professionnelle propre
1–25 Signaux mineurs (datacenter, VPN)
25–50 Risque modéré (signaux multiples)
50+ Risque élevé (Tor, abus connu, proxy) — bloquer ou exiger une vérification supplémentaire

Dans les logs de notre client, les IPs attaquantes se regroupaient en plages /24 avec des scores au-dessus de 50. Bloquer le sous-réseau après des abus répétés, pas seulement l'IP isolée, a réduit le volume rapidement.

Priorité 3 : intégrer FingerprintJS

Installez FingerprintJS sur vos pages d'inscription et de connexion. Stockez le visitorId à chaque tentative.

import FingerprintJS from "@fingerprintjs/fingerprintjs";

async function getFingerprint(): Promise<string> {
  const fp = await FingerprintJS.load();
  const result = await fp.get();
  return result.visitorId;
}

Puis appliquez des rate limits sur l'IP et le fingerprint :

  • Après 5 tentatives d'inscription ou de connexion échouées depuis le même fingerprint ou la même IP → blocage 24 heures.
  • Loggez les deux valeurs à chaque tentative pour pivoter facilement dans le back office.

Priorité 4 : valider l'email de manière asynchrone

Les emails jetables n'arrêteront pas un attaquant déterminé, mais ils filtrent une large part de l'abus low-effort. Appelez l'API Veille Email Validation en parallèle du check 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();
}

// Dans le handler d'inscription :
const [ipResult, emailResult] = await Promise.all([
  scoreIp(clientIp),
  validateEmail(email),
]);

if (emailResult.disposable || emailResult.risk_score > 70) {
  await blockIpTemporarily(clientIp, "disposable_email");
  return { error: "Inscription bloquée" };
}

if (ipResult.threat_score >= 50) {
  await blockIpTemporarily(clientIp, "high_threat_ip");
  return { error: "Inscription bloquée" };
}

Lancez ces checks après réception du formulaire mais avant d'accorder des crédits. Si un signal échoue, bloquez l'IP pour une durée configurable et renvoyez une erreur générique.

Priorité 5 : lier le fingerprint à la vérification email

Créer un compte n'est pas la même chose que prouver qu'on contrôle la boîte mail. Exigez la vérification email avant que les crédits soient utilisables.

L'étape que la plupart des équipes oublient : le fingerprint à l'inscription doit correspondre au fingerprint au clic sur le lien de vérification.

// À l'inscription — stocker la vérification en attente
await db.pendingVerifications.create({
  email,
  fingerprintSignup: fingerprint,
  ipSignup: clientIp,
  token: verificationToken,
  expiresAt: addHours(new Date(), 24),
});

// À la vérification — comparer le fingerprint
const pending = await db.pendingVerifications.findByToken(token);

if (pending.fingerprintSignup !== currentFingerprint) {
  await flagAccount(pending.email, "fingerprint_mismatch");
  return { error: "Vérification échouée" };
}

await activateAccount(pending.email);

Un mismatch ne signifie pas toujours une fraude — les utilisateurs changent d'appareil — mais des mismatches répétés depuis des IPs à haut risque sont un signal fort.

Priorité 6 : ne plus donner tous les crédits à l'inscription

Les crédits gratuits sont le ROI de l'attaquant. Répartissez la valeur sur des actions difficiles à automatiser :

Étape Crédits Pourquoi
Compte créé 0 Aucune valeur immédiate à extraire
Email vérifié +10 Prouve le contrôle de la boîte
Onboarding terminé +25 Nécessite une vraie interaction produit
Partage sur les réseaux +15 Ajoute de la friction et une trace d'audit

Exemple de flags dans le back office :

interface UserCredits {
  emailVerified: boolean;      // +10 crédits
  onboardingCompleted: boolean; // +25 crédits
  sharedOnSocial: boolean;      // +15 crédits
}

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;
}

Les bots optimisent le chemin le plus court vers les crédits. Un onboarding avec de vrais appels API, des choix de configuration ou un tutoriel court casse les flux scriptés.

Vue d'ensemble du flux d'inscription

L'utilisateur soumet le formulaire
        │
        ▼
Lire CF-Connecting-IP
        │
        ▼
Vérifier rate limits fingerprint + IP ──► bloqué ? → 429, log
        │
        ▼
Créer le compte (0 crédit)
        │
        ├──► async : réputation IP
        └──► async : validation email
                    │
                    ▼
              menace détectée ? → bloquer IP, flag compte
                    │
                    ▼
Envoyer email de vérification (stocker fingerprint signup)
        │
        ▼
Clic sur le lien → fingerprint identique ? → +10 crédits
        │
        ▼
Onboarding terminé → +25 crédits
        │
        ▼
Partage social confirmé → +15 crédits

Booleans utiles dans le back office

Donnez à votre équipe des toggles pour réagir sans déployer :

  • block_disposable_emails — rejeter les boîtes jetables à l'inscription
  • block_high_threat_ips — bloquer auto les IPs avec threat_score >= 50
  • require_fingerprint_match — exiger la correspondance fingerprint à la vérification email
  • rate_limit_by_fingerprint — activer le throttling par fingerprint
  • grant_credits_on_signup — laisser à false pendant une attaque active

Ces flags permettent de resserrer les défenses pendant un incident et de les assouplir une fois le trafic normalisé.

Dernier recours : ralentir sans alerter

Si une IP est clairement abusive mais que vous voulez éviter de signaler l'attaque, certaines équipes introduisent une friction artificielle — réponses retardées, CAPTCHA dès la deuxième tentative, ou un faux code SMS qui n'arrive jamais. C'est controversé, à réserver en dernier recours, clairement loggé en interne, et jamais montré aux utilisateurs légitimes.

L'objectif est de brûler le temps de l'attaquant, pas le vôtre.

Résultats

Après mise en place de cette stack — extraction IP réelle, checks Veille IP et email, rate limits FingerprintJS, vérification liée au fingerprint et crédits progressifs — le volume d'inscriptions abusives du client a chuté nettement en 48 heures. Les utilisateurs légitimes ont terminé l'onboarding sans remarquer les contrôles supplémentaires.

Articles liés