Les injections SQL : la faille n°1 des sites web
Une injection SQL se produit quand un attaquant insère du code SQL malveillant dans un champ de formulaire, et que votre code PHP l’envoie directement à la base de données sans le filtrer. C’est la faille de sécurité la plus courante et la plus dangereuse — elle permet de voler, modifier ou supprimer toutes vos données. Ce tutoriel vous montre comment vous en protéger définitivement.
Le code vulnérable : ne faites JAMAIS ça
// ❌ DANGEREUX — NE JAMAIS UTILISER EN PRODUCTION
$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
L’attaque : si un utilisateur entre ' OR '1'='1 comme nom d’utilisateur, la requête SQL devient :
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''
La condition '1'='1' est toujours vraie → l’attaquant accède au premier compte de la base (souvent l’administrateur). Avec '; DROP TABLE users; --, il peut supprimer toute votre table.
La solution : les requêtes préparées (prepared statements)
Les requêtes préparées séparent le code SQL des données. La base de données reçoit d’abord la structure de la requête, puis les valeurs séparément. Même si la valeur contient du SQL malveillant, elle est traitée comme une simple chaîne de texte.
Avec PDO (recommandé)
// Connexion avec PDO
$pdo = new PDO(
'mysql:host=localhost;dbname=mabase;charset=utf8mb4',
'utilisateur',
'motdepasse',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false // Requêtes préparées natives
]
);
// ✅ SÉCURISÉ — Requête préparée avec paramètres nommés
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND password = :password');
$stmt->execute([
':username' => $_POST['username'],
':password' => $_POST['password']
]);
$user = $stmt->fetch();
Avec MySQLi
// ✅ SÉCURISÉ — Requête préparée avec MySQLi
$stmt = $conn->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->bind_param('ss', $_POST['username'], $_POST['password']);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
PDO vs MySQLi : utilisez PDO. Il supporte 12 bases de données différentes (MySQL, PostgreSQL, SQLite…), a une API plus propre, et les paramètres nommés (:username) sont plus lisibles que les ?.
Sécurisation complète d’un formulaire
La protection contre les injections SQL n’est qu’une partie de la sécurisation. Voici la checklist complète :
1. Valider les entrées côté serveur
// Vérifier que les champs existent et ne sont pas vides
if (empty($_POST['email']) || empty($_POST['password'])) {
die('Tous les champs sont requis');
}
// Valider le format email
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
die('Email invalide');
}
// Limiter la longueur
if (strlen($_POST['password']) < 8 || strlen($_POST['password']) > 128) {
die('Le mot de passe doit faire entre 8 et 128 caractères');
}
2. Hasher les mots de passe
// À l'inscription : hasher le mot de passe
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
$stmt = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (:email, :hash)');
$stmt->execute([':email' => $email, ':hash' => $hash]);
// À la connexion : vérifier le hash
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
$user = $stmt->fetch();
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
// Connexion réussie
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
} else {
// Échec — message volontairement vague
echo 'Email ou mot de passe incorrect';
}
Règles critiques :
- Ne stockez JAMAIS les mots de passe en clair ou en MD5/SHA1
password_hash()utilise bcrypt par défaut — c’est le standard actuelpassword_verify()compare de manière sécurisée (résistant aux attaques timing)- Le message d’erreur ne doit pas dire si c’est l’email ou le mot de passe qui est faux
3. Protéger contre le CSRF
// Générer un token CSRF
session_start();
$csrf_token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $csrf_token;
// Dans le formulaire HTML
echo '<input type="hidden" name="csrf_token" value="' . $csrf_token . '">';
// À la soumission : vérifier le token
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
die('Requête invalide');
}
4. Échapper l’affichage (anti-XSS)
// Quand vous affichez des données utilisateur dans le HTML
echo htmlspecialchars($user['name'], ENT_QUOTES, 'UTF-8');
Checklist de sécurité formulaire
| Protection | Contre | Comment |
|---|---|---|
| Requêtes préparées | Injection SQL | PDO avec paramètres |
| Validation input | Données invalides | filter_var, strlen, regex |
| password_hash | Vol de mots de passe | Bcrypt automatique |
| Token CSRF | Requêtes forgées | Token unique par session |
| htmlspecialchars | XSS | Échapper l’affichage |
| HTTPS | Interception réseau | Certificat SSL (Let’s Encrypt) |
| Rate limiting | Brute force | Limiter les tentatives de connexion |
Exercice
Créez un formulaire d’inscription complet et sécurisé :
- Champs : email, mot de passe, confirmation du mot de passe
- Validation côté serveur de tous les champs
- Requête préparée PDO pour l’insertion
- Mot de passe hashé avec password_hash()
- Protection CSRF
- Messages d’erreur clairs mais qui ne révèlent pas d’informations sensibles