Polar Code 🎭

Command Palette

Search for a command to run...

10
Pièce N°10

Module 10 : Sécurité - Les Murs Invisibles

La pièce était nue. Juste un bureau, un ordinateur, et le poids du silence. La sécurité, c'était ça : des murs qu'on ne voyait pas. Des barrières entre le monde extérieur et le système. Chaque formulaire, chaque upload, chaque cookie était une porte. Et chaque porte pouvait être forcée. En PHP, il fallait construire ces murs. Les renforcer. Surveiller les failles.

Validation des Entrées - Le Filtrage

<?php
// validation.php
class Validateur {
    
    public function nettoyer($donnee) {
        $donnee = trim($donnee);
        $donnee = stripslashes($donnee);
        $donnee = htmlspecialchars($donnee, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        return $donnee;
    }
    
    public function validerEmail($email) {
        $email = filter_var($email, FILTER_SANITIZE_EMAIL);
        return filter_var($email, FILTER_VALIDATE_EMAIL);
    }
    
    public function validerNombre($nombre, $min = 0, $max = PHP_INT_MAX) {
        if (!is_numeric($nombre)) return false;
        $nombre = filter_var($nombre, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
        return ($nombre >= $min && $nombre <= $max);
    }
    
    public function validerTelephone($tel) {
        $tel = preg_replace('/[^0-9+]/', '', $tel);
        return preg_match('/^(\+[0-9]{1,3})?[0-9]{9,15}$/', $tel);
    }
    
    public function validerFichier($fichier, $extensionsAutorisees = ['jpg', 'png', 'pdf']) {
        $nom = basename($fichier['name']);
        $extension = strtolower(pathinfo($nom, PATHINFO_EXTENSION));
        
        if (!in_array($extension, $extensionsAutorisees)) {
            return false;
        }
        
        // Vérifier le type MIME réel
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $fichier['tmp_name']);
        finfo_close($finfo);
        
        $mimesAutorises = [
            'jpg' => 'image/jpeg',
            'png' => 'image/png',
            'pdf' => 'application/pdf'
        ];
        
        return ($mimesAutorises[$extension] ?? '') === $mime;
    }
}

// Utilisation
$validateur = new Validateur();

$email = "test@<script>alert('xss')</script>.com";
$emailPropre = $validateur->nettoyer($email);
$emailValide = $validateur->validerEmail($emailPropre);

echo $emailValide ? "Email valide\n" : "Email invalide\n";
?>

Hashage des Mots de Passe - Les Sceaux

<?php
// passwords.php
class GardeMotDePasse {
    
    public function hacher($motdepasse) {
        // Utilisation de password_hash avec l'algorithme par défaut (bcrypt)
        return password_hash($motdepasse, PASSWORD_DEFAULT);
    }
    
    public function verifier($motdepasse, $hash) {
        return password_verify($motdepasse, $hash);
    }
    
    public function doitEtreRehash($hash) {
        return password_needs_rehash($hash, PASSWORD_DEFAULT);
    }
    
    public function genererTokenSecurise() {
        return bin2hex(random_bytes(32)); // 64 caractères hexadécimaux
    }
    
    public function hacherAvecPepper($motdepasse, $pepper) {
        // Pepper global (stocké hors du code, dans .env par exemple)
        $motdepassePeppered = hash_hmac('sha256', $motdepasse, $pepper);
        return password_hash($motdepassePeppered, PASSWORD_DEFAULT);
    }
    
    public function verifierAvecPepper($motdepasse, $hash, $pepper) {
        $motdepassePeppered = hash_hmac('sha256', $motdepasse, $pepper);
        return password_verify($motdepassePeppered, $hash);
    }
}

// Utilisation
$garde = new GardeMotDePasse();
$pepper = file_get_contents('/chemin/secret/pepper.key'); // Lecture depuis fichier sécurisé

$motdepasseUtilisateur = "MonSecret123";
$hash = $garde->hacherAvecPepper($motdepasseUtilisateur, $pepper);

// Stocker $hash en base de données

// Vérification plus tard
$motdepasseSaisi = "MonSecret123";
$verifie = $garde->verifierAvecPepper($motdepasseSaisi, $hash, $pepper);

echo $verifie ? "Accès autorisé\n" : "Accès refusé\n";

// Génération de token pour réinitialisation
$tokenReset = $garde->genererTokenSecurise();
echo "Token : $tokenReset\n";
?>

Protection XSS - Nettoyer la Sortie

<?php
// xss.php
class ProtegeurXSS {
    
    public function echapper($donnee) {
        if (is_array($donnee)) {
            return array_map([$this, 'echapper'], $donnee);
        }
        
        return htmlspecialchars($donnee, ENT_QUOTES | ENT_HTML5, 'UTF-8', false);
    }
    
    public function nettoyerPourHTML($html) {
        $config = HTMLPurifier_Config::createDefault();
        $purifier = new HTMLPurifier($config);
        return $purifier->purify($html);
    }
    
    public function enTetesSecurite() {
        // Headers de sécurité
        header('X-Content-Type-Options: nosniff');
        header('X-Frame-Options: DENY');
        header('X-XSS-Protection: 1; mode=block');
        header('Referrer-Policy: strict-origin-when-cross-origin');
        
        // CSP (Content Security Policy)
        $csp = "default-src 'self';";
        $csp .= " script-src 'self' 'unsafe-inline' https://trusted.cdn.com;";
        $csp .= " style-src 'self' 'unsafe-inline';";
        $csp .= " img-src 'self' data: https:;";
        $csp .= " font-src 'self';";
        $csp .= " frame-ancestors 'none';";
        
        header("Content-Security-Policy: " . $csp);
    }
    
    public function protectionSession() {
        session_start([
            'cookie_httponly' => true,
            'cookie_secure' => true, // En HTTPS seulement
            'cookie_samesite' => 'Strict',
            'use_strict_mode' => true
        ]);
        
        // Régénération d'ID de session
        if (!isset($_SESSION['created'])) {
            $_SESSION['created'] = time();
        } elseif (time() - $_SESSION['created'] > 1800) { // 30 minutes
            session_regenerate_id(true);
            $_SESSION['created'] = time();
        }
    }
}

// Utilisation
$protegeur = new ProtegeurXSS();
$protegeur->enTetesSecurite();

// Donnée potentiellement dangereuse
$commentaireUtilisateur = "<script>alert('XSS')</script><p>Bonjour</p>";

// Échappement pour texte simple
echo $protegeur->echapper($commentaireUtilisateur) . "\n";

// Si HTML est autorisé (éditeur riche), nettoyer avec HTMLPurifier
// echo $protegeur->nettoyerPourHTML($commentaireUtilisateur);
?>

Protection CSRF - Les Tokens Cachés

<?php
// csrf.php
class BouclierCSRF {
    
    public function genererToken() {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public function verifierToken($tokenSoumis) {
        if (empty($_SESSION['csrf_token']) || empty($tokenSoumis)) {
            return false;
        }
        
        return hash_equals($_SESSION['csrf_token'], $tokenSoumis);
    }
    
    public function champCache() {
        $token = $this->genererToken();
        return '<input type="hidden" name="csrf_token" value="' . $token . '">';
    }
    
    public function validerRequete() {
        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            if (!$this->verifierToken($_POST['csrf_token'] ?? '')) {
                http_response_code(403);
                die("Requête non autorisée.");
            }
        }
    }
    
    public function tokenPourAPI() {
        $token = $this->genererToken();
        $_SESSION['api_csrf_token'] = $token;
        return $token;
    }
    
    public function verifierTokenAPI($token) {
        return isset($_SESSION['api_csrf_token']) && 
               hash_equals($_SESSION['api_csrf_token'], $token);
    }
}

// Utilisation dans un formulaire
session_start();
$csrf = new BouclierCSRF();

// Dans le formulaire HTML
// echo $csrf->champCache();

// Au traitement
// $csrf->validerRequete();
?>

Bonnes Pratiques - Les Règles Non Écrites

<?php
// bonnes_pratiques.php
class PoliceSecurite {
    
    // 1. Configuration sécurisée
    public function configSecurite() {
        // Désactiver l'affichage des erreurs en production
        ini_set('display_errors', '0');
        ini_set('log_errors', '1');
        ini_set('error_log', '/chemin/securise/php_errors.log');
        
        // Limiter les exécutions
        ini_set('max_execution_time', '30');
        ini_set('memory_limit', '128M');
        
        // Headers de sécurité
        header_remove('X-Powered-By');
        header_remove('Server');
    }
    
    // 2. Protection des dossiers
    public function protegerDossiers() {
        // .htaccess automatique pour interdire l'accès
        $htaccess = "Order Deny,Allow\nDeny from all\n";
        $dossiers = ['config', 'logs', 'uploads', 'classes'];
        
        foreach ($dossiers as $dossier) {
            if (!file_exists($dossier . '/.htaccess')) {
                file_put_contents($dossier . '/.htaccess', $htaccess);
            }
        }
        
        // Fichier index.php vide dans les dossiers sensibles
        $indexContent = "<?php\n// Accès interdit\nhttp_response_code(403);\nexit;";
        foreach ($dossiers as $dossier) {
            if (!file_exists($dossier . '/index.php')) {
                file_put_contents($dossier . '/index.php', $indexContent);
            }
        }
    }
    
    // 3. Validation des uploads
    public function uploadSecurise($fichier) {
        // Vérifier les erreurs
        if ($fichier['error'] !== UPLOAD_ERR_OK) {
            return false;
        }
        
        // Vérifier la taille
        if ($fichier['size'] > 5 * 1024 * 1024) { // 5Mo max
            return false;
        }
        
        // Vérifier le type MIME
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mime = finfo_file($finfo, $fichier['tmp_name']);
        finfo_close($finfo);
        
        $typesAutorises = [
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'application/pdf' => 'pdf'
        ];
        
        if (!isset($typesAutorises[$mime])) {
            return false;
        }
        
        // Renommer le fichier
        $extension = $typesAutorises[$mime];
        $nouveauNom = bin2hex(random_bytes(16)) . '.' . $extension;
        $destination = 'uploads/' . $nouveauNom;
        
        // Déplacer avec vérification
        if (move_uploaded_file($fichier['tmp_name'], $destination)) {
            // Changer les permissions
            chmod($destination, 0644);
            return $nouveauNom;
        }
        
        return false;
    }
    
    // 4. Protection des sessions
    public function sessionSecurisee() {
        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.use_only_cookies', '1');
        
        // Durée de session
        ini_set('session.gc_maxlifetime', '1800'); // 30 minutes
        session_set_cookie_params(1800, '/', '', true, true);
    }
    
    // 5. Protection SQL
    public function requeteSecurisee($pdo, $sql, $params = []) {
        $stmt = $pdo->prepare($sql);
        foreach ($params as $key => $value) {
            $type = is_int($value) ? PDO::PARAM_INT : PDO::PARAM_STR;
            $stmt->bindValue($key, $value, $type);
        }
        $stmt->execute();
        return $stmt;
    }
    
    // 6. Rate limiting basique
    public function verifierRateLimit($ip, $limite = 100, $periode = 3600) {
        $cle = 'rate_limit_' . md5($ip);
        $compteur = apcu_fetch($cle, $success);
        
        if (!$success) {
            apcu_store($cle, 1, $periode);
            return true;
        }
        
        if ($compteur >= $limite) {
            return false;
        }
        
        apcu_inc($cle);
        return true;
    }
}

// Utilisation
$police = new PoliceSecurite();
$police->configSecurite();
$police->protegerDossiers();
$police->sessionSecurisee();

// Test rate limiting
if (!$police->verifierRateLimit($_SERVER['REMOTE_ADDR'])) {
    http_response_code(429);
    die("Trop de requêtes.");
}
?>

Système Complet - Le Bastion

<?php
// securite_complet.php
require_once 'validation.php';
require_once 'csrf.php';
require_once 'bonnes_pratiques.php';

class Bastion {
    private $validateur;
    private $csrf;
    private $police;
    
    public function __construct() {
        $this->validateur = new Validateur();
        $this->csrf = new BouclierCSRF();
        $this->police = new PoliceSecurite();
        
        $this->police->configSecurite();
        $this->police->sessionSecurisee();
    }
    
    public function traiterFormulaire($donnees) {
        // 1. Validation
        $donneesNettoyees = [];
        foreach ($donnees as $cle => $valeur) {
            $donneesNettoyees[$cle] = $this->validateur->nettoyer($valeur);
        }
        
        // 2. Vérification CSRF
        if (!$this->csrf->verifierToken($donneesNettoyees['csrf_token'] ?? '')) {
            throw new Exception("Token CSRF invalide.");
        }
        
        // 3. Rate limiting
        if (!$this->police->verifierRateLimit($_SERVER['REMOTE_ADDR'])) {
            throw new Exception("Limite de requêtes atteinte.");
        }
        
        // 4. Validation spécifique
        if (isset($donneesNettoyees['email'])) {
            if (!$this->validateur->validerEmail($donneesNettoyees['email'])) {
                throw new Exception("Email invalide.");
            }
        }
        
        return $donneesNettoyees;
    }
    
    public function telechargerFichier($fichier) {
        $resultat = $this->police->uploadSecurise($fichier);
        
        if (!$resultat) {
            throw new Exception("Échec du téléchargement sécurisé.");
        }
        
        // Journaliser
        error_log("Fichier uploadé: $resultat par " . $_SERVER['REMOTE_ADDR']);
        
        return $resultat;
    }
    
    public function genererReponseSecurisee($donnees) {
        // Headers
        header('Content-Type: application/json; charset=utf-8');
        
        // Échapper les données
        $donneesEchappees = $this->validateur->echapper($donnees);
        
        // Ajouter token CSRF pour la prochaine requête
        $donneesEchappees['csrf_token'] = $this->csrf->genererToken();
        
        return json_encode($donneesEchappees);
    }
}

// Utilisation
try {
    $bastion = new Bastion();
    
    // Traitement sécurisé
    $donneesSecurisees = $bastion->traiterFormulaire($_POST);
    
    // Réponse sécurisée
    echo $bastion->genererReponseSecurisee([
        'success' => true,
        'message' => 'Opération réussie'
    ]);
    
} catch (Exception $e) {
    error_log("Erreur sécurité: " . $e->getMessage());
    
    http_response_code(400);
    echo json_encode([
        'success' => false,
        'error' => 'Erreur de traitement'
    ]);
}
?>

Je soufflai la fumée de ma cigarette. La sécurité. Un travail invisible. Des validations qu'on ne voyait pas. Des hash qu'on ne déchiffrait pas. Des tokens qu'on devait vérifier. Chaque ligne de code était un maillon. Chaque maillon devait tenir. Parce que dans l'ombre, une faille, c'était une porte ouverte. Et derrière chaque porte, il y avait quelqu'un qui attendait. Quelqu'un avec de mauvaises intentions. La sécurité, c'était pas une option. C'était la première règle. La dernière aussi.