Sécurité — injection SQL, utilisateurs, sauvegardes
Injection SQL (comment ça marche, démonstration, requêtes préparées). Utilisateurs MySQL (CREATE USER, GRANT, REVOKE, principe moindre privilège). Sauvegardes (mysqldump, restauration, test). Mots de passe hashés.
Concepts Théoriques
Une base de données contient les informations les plus sensibles d'une entreprise : emails clients, numéros de téléphone, historiques d'achats, adresses, montants dépensés. Une seule faille de sécurité peut exposer tout cela — atteinte à la réputation, perte de clients, amendes légales. Ce chapitre couvre les 3 piliers de la sécurité en base de données.
Pilier 1 — L'injection SQL (la menace n°1)
L'injection SQL est classée n°3 des vulnérabilités web les plus dangereuses par l'OWASP (Open Web Application Security Project). Elle se produit quand un attaquant insère du code SQL malveillant via un formulaire web ou une URL.
Comment ça marche — démonstration pas à pas :
Imaginez un formulaire de connexion. Le code PHP naïf construit la requête par concaténation :
$sql = "SELECT * FROM users WHERE email = '$email' AND password = '$password'";
L'utilisateur normal tape fatou@email.com et son mot de passe → la requête est : SELECT * FROM users WHERE email = 'fatou@email.com' AND password = 'motdepasse'; — tout fonctionne.
L'attaquant tape dans le champ email : ' OR 1=1 --
La requête devient : SELECT * FROM users WHERE email = '' OR 1=1 --' AND password = ''
Décryptage : le ' ferme le guillemet de email. OR 1=1 est toujours vrai (donc la condition WHERE est toujours vraie). Le -- commente tout le reste (AND password = ...). Résultat : la requête retourne TOUS les utilisateurs. L'attaquant est connecté sans mot de passe.
Pire encore : avec '; DROP TABLE users; -- dans le champ, la requête exécute d'abord le SELECT (qui échoue), puis DROP TABLE users; — la table entière est supprimée.
Avec ' UNION SELECT email, password, NULL FROM users -- l'attaquant extrait tous les emails et mots de passe de la base.
La solution : les requêtes préparées
Les requêtes préparées séparent le code SQL des données. MySQL sait que la valeur fournie est une DONNÉE, jamais du CODE — même si elle contient du SQL.
En PHP/PDO : $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ? AND password = ?"); $stmt->execute([$email, $password]);
En Laravel/Eloquent : User::where('email', $email)->first(); — Eloquent utilise des requêtes préparées automatiquement.
Même si $email contient ' OR 1=1 --, il sera traité comme du texte littéral, pas du SQL. MySQL cherchera un utilisateur dont l'email est exactement "' OR 1=1 --" — et n'en trouvera aucun.
Règle absolue : JAMAIS de concaténation de variables dans le SQL. TOUJOURS des requêtes préparées. Sans exception. Dans tous les langages (PHP, Python, Node.js, Java, C#).
Pilier 2 — Utilisateurs et privilèges MySQL
L'utilisateur root a TOUS les droits : créer des bases, supprimer des tables, créer d'autres utilisateurs, et même arrêter le serveur MySQL. Connecter votre application web avec root en production, c'est comme donner les clés du coffre-fort à tous les employés.
Le principe du moindre privilège : chaque utilisateur n'a que les droits strictement nécessaires pour sa tâche. L'application web a besoin de SELECT, INSERT, UPDATE, DELETE — pas de DROP TABLE, ALTER TABLE, ni CREATE USER.
MySQL gère les privilèges à 4 niveaux :
- GLOBAL : s'applique à toutes les bases (GRANT ALL ON .)
- DATABASE : s'applique à une base (GRANT SELECT ON sunushop.*)
- TABLE : s'applique à une table (GRANT SELECT ON sunushop.products)
- COLUMN : s'applique à des colonnes spécifiques (GRANT SELECT (name, price) ON sunushop.products)
En pratique, le niveau DATABASE est le plus courant : l'application accède à toute la base sunushop mais ne peut pas toucher les autres bases.
Pilier 3 — Sauvegardes
La seule protection contre une suppression accidentelle ("j'ai fait DELETE sans WHERE..."), une corruption de données, un disque dur défaillant, ou un ransomware. Sans sauvegarde, tout est perdu.
mysqldump exporte la base dans un fichier SQL — un fichier texte contenant les CREATE TABLE et INSERT nécessaires pour recréer la base à l'identique.
Stratégie de sauvegarde en production :
- Sauvegarde automatique quotidienne (cron job à 3h du matin)
- Stockage sur un serveur DIFFÉRENT (si le serveur principal meurt, la sauvegarde est safe)
- Rétention de 30 jours (garder les 30 dernières sauvegardes, supprimer les plus anciennes)
- Test de restauration mensuel — une sauvegarde que vous ne pouvez pas restaurer est inutile
Mots de passe — JAMAIS en clair
Ne stockez JAMAIS un mot de passe en clair dans la base. Si la base est volée, tous les comptes sont compromis. Utilisez un hash irréversible (bcrypt ou Argon2) côté application :
- PHP : password_hash($password, PASSWORD_DEFAULT)
- Laravel : Hash::make($password)
- Vérification : password_verify($input, $hash)
Le hash est à sens unique — même avec le hash, on ne peut pas retrouver le mot de passe original. MySQL ne fait PAS le hashage — c'est la responsabilité de l'application.
Logs et audit
MySQL peut enregistrer toutes les requêtes exécutées :
- general_log — log toutes les requêtes (très verbeux, utiliser en dev seulement)
- slow_query_log — log les requêtes plus lentes qu'un seuil (ex: 1 seconde). Indispensable en production pour identifier les requêtes à optimiser.
Stored procedures et triggers — pour aller plus loin
MySQL permet d'écrire de la logique directement DANS la base de données :
Les stored procedures sont des fonctions SQL stockées dans la base. Au lieu d'envoyer 10 requêtes depuis PHP, vous appelez une procédure qui les exécute toutes côté serveur. Exemple : une procédure sp_create_order qui insère la commande, les lignes, décrémente le stock et crée le paiement — le tout dans une transaction.
-- Syntaxe de base (pour information — on n'en crée pas dans ce cours)
DELIMITER //
CREATE PROCEDURE sp_product_stats(IN cat_id INT)
BEGIN
SELECT COUNT(*) AS nb, AVG(price) AS avg_price
FROM products WHERE category_id = cat_id;
END //
DELIMITER ;
-- Appel
CALL sp_product_stats(1);Les triggers sont des actions automatiques qui se déclenchent AVANT ou APRÈS un INSERT, UPDATE, ou DELETE. Exemple : un trigger qui met à jour orders.total automatiquement chaque fois qu'on insère un order_item.
-- Syntaxe de base (pour information)
CREATE TRIGGER trg_update_order_total
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
UPDATE orders SET total = (
SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = NEW.order_id
) WHERE id = NEW.order_id;
END;Ces fonctionnalités sont puissantes mais ajoutent de la complexité. En pratique, la tendance moderne (Laravel, Django, Rails) est de gérer cette logique côté application plutôt que dans la base. Mais vous les rencontrerez en entreprise sur des bases existantes — il est important de savoir qu'elles existent et comment les lire.