Le problème
En 2026, un simple formulaire de contact reste l'une des surfaces d'attaque les plus exploitées sur le web. Les bots sont plus sophistiqués, les attaques par force brute plus fréquentes, et un token CSRF seul ne suffit plus. Sur ce site, j'ai implémenté une stratégie de défense en profondeur avec quatre couches complémentaires.
Couche 1 : Token CSRF avec rotation
Le classique, mais correctement implémenté. Un token unique par session, régénéré à chaque soumission réussie, avec validation côté serveur via hash_equals() pour éviter les timing attacks :
<?php
final class CsrfGuard
{
public function generateToken(): string
{
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_time'] = time();
return $token;
}
public function validateToken(string $submitted): bool
{
$stored = $_SESSION['csrf_token'] ?? '';
if (!hash_equals($stored, $submitted)) {
return false;
}
// Invalider après usage (one-time token)
unset($_SESSION['csrf_token']);
return true;
}
}
L'utilisation de hash_equals() est essentielle : une comparaison classique avec === peut fuiter des informations de timing exploitables par un attaquant.
Couche 2 : Honeypot invisible
Un champ caché en CSS (pas en display:none que certains bots détectent) piège les robots qui remplissent tous les champs automatiquement :
// Dans le template
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input type="text" name="website" tabindex="-1" autocomplete="off">
</div>
// Dans le contrôleur
if (!empty($_POST['website'])) {
// C'est un bot — on retourne un faux succès
http_response_code(200);
exit;
}
Le faux succès en réponse est intentionnel : retourner une erreur 4xx informerait le bot qu'il a été détecté, lui permettant d'adapter sa stratégie.
Couche 3 : Rate Limiting par IP
Sans base de données, j'utilise le système de fichiers pour limiter les soumissions par IP. Simple, efficace, et sans dépendance externe :
final class RateLimiter
{
public function __construct(
private readonly string $storageDir,
private readonly int $maxAttempts = 3,
private readonly int $windowSeconds = 3600,
) {}
public function isAllowed(string $ip): bool
{
$file = $this->storageDir . '/' . md5($ip) . '.json';
if (!file_exists($file)) {
return true;
}
$data = json_decode(file_get_contents($file), true);
$recent = array_filter(
$data['attempts'] ?? [],
fn(int $t) => $t > time() - $this->windowSeconds
);
return count($recent) < $this->maxAttempts;
}
}
En production, un nettoyage CRON des fichiers expirés complète le dispositif. Pour un site à fort trafic, Redis ou Memcached seraient préférables.
Couche 4 : Time Check (anti-bot rapide)
Un humain met au minimum 3 à 5 secondes pour remplir un formulaire. Un bot le fait en millisecondes. On enregistre le timestamp de l'affichage du formulaire et on vérifie la durée côté serveur :
// À la génération du formulaire
$_SESSION['form_rendered_at'] = time();
// À la soumission
$elapsed = time() - ($_SESSION['form_rendered_at'] ?? time());
if ($elapsed < 3) {
// Soumission trop rapide — probablement un bot
Logger::warning('Bot detected: form submitted in {s}s', ['s' => $elapsed]);
return $this->fakeSuccess();
}
Orchestration : le middleware de validation
Ces quatre couches sont orchestrées dans un middleware unique qui les exécute séquentiellement. Si l'une échoue, la requête est rejetée (ou un faux succès est retourné pour les bots) :
final class FormSecurityMiddleware
{
public function process(Request $request): ValidationResult
{
// 1. Rate limit
if (!$this->rateLimiter->isAllowed($request->ip())) {
return ValidationResult::blocked('rate_limit');
}
// 2. Honeypot
if (!empty($request->post('website'))) {
return ValidationResult::silent();
}
// 3. Time check
if ($this->isSubmittedTooFast()) {
return ValidationResult::silent();
}
// 4. CSRF
if (!$this->csrf->validateToken($request->post('_token', ''))) {
return ValidationResult::blocked('csrf');
}
return ValidationResult::passed();
}
}
Conclusion
Aucune de ces couches n'est infaillible individuellement. Un CSRF seul ne bloque pas les bots. Un honeypot seul ne résiste pas aux bots ciblés. Mais combinées, elles forment une défense en profondeur qui rend l'exploitation prohibitivement coûteuse. C'est le principe fondamental de la sécurité : augmenter le coût de l'attaque jusqu'à ce qu'elle ne soit plus rentable.
Pour un site vitrine sans base de données, cette approche offre un excellent rapport sécurité/complexité. Pour une application critique, ajoutez un WAF, une validation CAPTCHA progressive, et des logs centralisés dans un SIEM.