Aller au contenu principal
← Retour aux articles

Sécuriser un formulaire PHP en 2026 : au-delà du CSRF

6 min de lecture

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.