Attaques par Injection : Comprendre et Prévenir les Vulnérabilités Courantes
Bienvenue dans cette leçon fondamentale sur les attaques par injection, un pilier de la sécurité des applications web et API. Dans le cadre de votre parcours pour Maîtriser la Sécurité des Applications Web et API : Protégez Vos Projets des Cybermenaces, il est impératif de comprendre comment ces vulnérabilités fonctionnent et, plus important encore, comment les prévenir efficacement.
Les attaques par injection sont constamment classées parmi les menaces les plus critiques par l'OWASP (Open Web Application Security Project), témoignant de leur persistance et de leur impact dévastateur. Que ce soit pour voler des données sensibles, corrompre des informations ou prendre le contrôle d'un système, les injections restent une voie privilégiée pour les attaquants.
Cette leçon vous guidera à travers la nature des attaques par injection, explorera les types les plus courants avec des exemples concrets, et vous fournira les stratégies de prévention essentielles pour protéger vos applications.
Qu'est-ce qu'une Attaque par Injection ?
Une attaque par injection se produit lorsqu'un attaquant envoie des données non fiables à une application, et que cette application exécute (ou interprète) ces données comme faisant partie d'une commande ou d'une requête. En substance, l'attaquant "injecte" son propre code ou ses propres commandes dans le flux de l'application, détournant ainsi son comportement normal.
Imaginez une application qui construit des commandes pour un interpréteur (comme une base de données, un système d'exploitation ou un parseur XML) en concaténant des chaînes de caractères, dont une partie provient directement de l'entrée utilisateur. Si l'entrée utilisateur n'est pas correctement validée, filtrée ou échappée, un attaquant peut insérer des caractères spéciaux qui modifient la structure de la commande originale, lui permettant d'exécuter des actions malveillantes.
Ces vulnérabilités sont souvent le résultat d'une séparation insuffisante entre les données et le code/les commandes.
Caractéristiques Communes
- Entrée non fiable : L'application accepte des données directement ou indirectement contrôlées par un utilisateur malveillant.
- Interprétation du contexte : Ces données sont utilisées dans un contexte où elles peuvent être interprétées comme du code ou des commandes (ex: une requête SQL, une commande shell, une instruction LDAP).
- Absence de défense : Il n'y a pas de validation d'entrée adéquate, d'échappement des caractères spéciaux ou de techniques de requêtes sécurisées.
Vulnérabilités d'Injection Courantes
Bien qu'il existe de nombreux types d'attaques par injection (LDAP Injection, XPath Injection, Mail Header Injection, etc.), nous allons nous concentrer sur les plus répandues et impactantes.
Injection SQL (SQLi)
L'injection SQL est sans doute le type d'attaque par injection le plus connu et le plus dévastateur. Elle permet à un attaquant d'exécuter des requêtes SQL arbitraires sur la base de données d'une application. Cela peut conduire à :
- La divulgation de toutes les données de la base.
- La modification ou la suppression de données.
- L'exécution de commandes système (dans certains cas, si le SGBD le permet).
- Le contournement des mécanismes d'authentification.
Comment ça marche ?
Une SQLi se produit lorsque l'application construit une requête SQL dynamique en y insérant directement des entrées utilisateur sans les neutraliser.
Exemple de code PHP vulnérable :
<?php
// Connexion à la base de données (exemple simplifié)
$conn = new mysqli("localhost", "utilisateur", "motdepasse", "mabasededonnees");
if ($conn->connect_error) {
die("Erreur de connexion : " . $conn->connect_error);
}
// Récupération de l'ID utilisateur à partir des paramètres GET
$user_id = $_GET['id']; // <-- Entrée utilisateur non filtrée
// Requête SQL vulnérable à l'injection
$sql = "SELECT username, email FROM users WHERE id = " . $user_id;
$result = $conn->query($sql);
if ($result && $result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
echo "Nom d'utilisateur: " . $row["username"]. " - Email: " . $row["email"]. "<br>";
}
} else {
echo "Aucun utilisateur trouvé ou erreur de requête.";
}
$conn->close();
?>
Explication de la vulnérabilité :
Dans cet exemple, la variable $user_id est directement concaténée dans la chaîne SQL. Si un attaquant fournit une valeur telle que 1 OR 1=1 pour $_GET['id'], la requête SQL devient :
SELECT username, email FROM users WHERE id = 1 OR 1=1
Cette requête retourne tous les utilisateurs de la table, car la condition 1=1 est toujours vraie. Un attaquant plus sophistiqué pourrait utiliser 1 UNION SELECT password, email FROM users pour extraire les mots de passe, ou 1; DROP TABLE users; pour supprimer la table (si le SGBD le permet et l'utilisateur de la base a les permissions).
Prévention de l'Injection SQL : Requêtes Préparées
La méthode la plus robuste et recommandée pour prévenir l'injection SQL est l'utilisation de requêtes préparées (Prepared Statements) avec des paramètres liés. Cette technique sépare clairement le code SQL des données utilisateur. Le SGBD reçoit d'abord le modèle de la requête, puis les données séparément, s'assurant que les données ne peuvent pas être interprétées comme du code SQL.
Exemple de code PHP sécurisé (avec PDO) :
<?php
// Connexion à la base de données avec PDO
$dsn = 'mysql:host=localhost;dbname=mabasededonnees;charset=utf8';
$username = 'utilisateur';
$password = 'motdepasse';
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Afficher les erreurs PDO
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC // Par défaut, récupérer les résultats en tableau associatif
]);
} catch (PDOException $e) {
die("Erreur de connexion PDO : " . $e->getMessage());
}
// Récupération de l'ID utilisateur à partir des paramètres GET
$user_id = $_GET['id'] ?? null; // Utilisation de l'opérateur de coalescence pour éviter les notices
if ($user_id) {
// Requête préparée avec un marqueur de position (?)
$sql = "SELECT username, email FROM users WHERE id = ?";
try {
$stmt = $pdo->prepare($sql);
// Liaison du paramètre (les données sont envoyées séparément et ne peuvent pas modifier la requête)
$stmt->execute([$user_id]);
if ($stmt->rowCount() > 0) {
while ($row = $stmt->fetch()) {
echo "Nom d'utilisateur: " . $row["username"] . " - Email: " . $row["email"] . "<br>";
}
} else {
echo "Aucun utilisateur trouvé.";
}
} catch (PDOException $e) {
echo "Erreur lors de l'exécution de la requête : " . $e->getMessage();
}
} else {
echo "ID utilisateur manquant.";
}
?>
Explication de la solution sécurisée :
Avec PDO et les requêtes préparées, même si un attaquant envoie 1 OR 1=1 pour $_GET['id'], cette chaîne sera traitée uniquement comme une valeur de données pour le paramètre id, et non comme du code SQL. La base de données recherchera un ID utilisateur qui est littéralement la chaîne "1 OR 1=1", ce qui ne correspondra probablement à aucun utilisateur, empêchant ainsi l'injection.
Injection de Commandes OS (OS Command Injection)
L'injection de commandes OS permet à un attaquant d'exécuter des commandes système arbitraires sur le serveur hébergeant l'application. Cela peut entraîner :
- Le contrôle total du serveur.
- L'accès, la modification ou la suppression de fichiers.
- Le déploiement de malwares ou de backdoors.
- L'exploration du réseau interne.
Comment ça marche ?
Cette vulnérabilité survient lorsqu'une application utilise des fonctions système (comme exec, system, shell_exec en PHP ; child_process.exec en Node.js ; os.system ou subprocess.call en Python) pour exécuter des commandes, et y insère directement des entrées utilisateur sans les valider ou les échapper.
Exemple de code Node.js vulnérable :
const express = require('express');
const { exec } = require('child_process');
const app = express();
const port = 3000;
app.get('/lookup', (req, res) => {
const domain = req.query.domain; // <-- Entrée utilisateur non filtrée
// Commande vulnérable à l'injection
// L'attaquant peut ajouter des commandes via le `;`, `&&`, `||`, etc.
const command = `ping -c 4 ${domain}`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Erreur d'exécution: ${error}`);
return res.status(500).send(`Erreur: ${error.message}`);
}
res.send(`<pre>${stdout}</pre><pre>${stderr}</pre>`);
});
});
app.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}`);
});
Explication de la vulnérabilité :
L'application permet à l'utilisateur de fournir un nom de domaine pour une commande ping. Si un attaquant envoie localhost; ls -la /, la variable domain prend cette valeur, et la commande ping devient :
ping -c 4 localhost; ls -la /
Le ; agit comme un séparateur de commandes sur les systèmes Unix/Linux, permettant d'exécuter ls -la / après le ping. Un attaquant pourrait ainsi lister des répertoires sensibles ou exécuter n'importe quelle commande.
Prévention de l'Injection de Commandes OS
La meilleure approche est d'éviter d'exécuter des commandes système externes basées sur des entrées utilisateur. Si c'est absolument nécessaire, suivez ces principes :
- Utilisez des API spécifiques et sécurisées : Privilégiez les fonctions qui ne passent pas par un shell, ou qui gèrent l'échappement des arguments.
- Validation d'entrée stricte (Whitelist) : N'acceptez que des valeurs connues et sûres (ex: des noms de fichiers ou des domaines valides) via une liste blanche. Rejetez tout ce qui n'est pas explicitement autorisé.
- Échappement des arguments : Si vous devez passer des arguments à un shell, assurez-vous qu'ils sont correctement échappés pour que les caractères spéciaux soient traités comme des littéraux et non comme des métacaractères de commande.
Exemple de code Node.js sécurisé (avec spawn et validation) :
const express = require('express');
const { spawn } = require('child_process'); // Utiliser spawn pour passer les arguments séparément
const app = express();
const port = 3000;
app.get('/lookup', (req, res) => {
const domain = req.query.domain;
if (!domain || !/^[a-zA-Z0-9.-]+$/.test(domain)) { // Validation stricte du domaine
return res.status(400).send('Nom de domaine invalide.');
}
// Utilisation de spawn pour exécuter la commande avec des arguments séparés
// Cela empêche l'interprétation des caractères spéciaux dans 'domain'
const ls = spawn('ping', ['-c', '4', domain]);
let stdoutData = '';
let stderrData = '';
ls.stdout.on('data', (data) => {
stdoutData += data.toString();
});
ls.stderr.on('data', (data) => {
stderrData += data.toString();
});
ls.on('close', (code) => {
if (code !== 0) {
console.error(`Processus ping terminé avec le code ${code}`);
return res.status(500).send(`Erreur d'exécution de la commande: ${stderrData || 'Erreur inconnue'}`);
}
res.send(`<pre>${stdoutData}</pre>`);
});
});
app.listen(port, () => {
console.log(`Serveur sécurisé démarré sur http://localhost:${port}`);
});
Explication de la solution sécurisée :
- Validation d'entrée : Une expression régulière
^[a-zA-Z0-9.-]+$est utilisée pour s'assurer que le nom de domaine ne contient que des caractères alpha-numériques, des points et des tirets, excluant ainsi tout caractère spécial qui pourrait être interprété comme une commande. child_process.spawn: Au lieu deexec(qui interprète la commande comme une chaîne shell),spawnexécute le programme directement avec des arguments passés comme un tableau séparé. Cela signifie quedomainest traité comme un simple argument et non comme une partie de la commande shell, empêchant ainsi l'injection.
Injection XML External Entity (XXE)
L'injection XXE est une vulnérabilité qui se produit lorsqu'un parseur XML traite une entrée XML contenant une référence à une entité externe définie par l'utilisateur. Un attaquant peut exploiter cette vulnérabilité pour :
- Divulgation de fichiers locaux : Lire des fichiers arbitraires sur le serveur (ex:
/etc/passwd). - Attaques par déni de service (DoS) : Utiliser des entités récursives pour consommer des ressources serveur.
- Server-Side Request Forgery (SSRF) : Forcer le serveur à effectuer des requêtes vers des systèmes internes ou externes.
- Dans certains cas, exécution de code à distance.
Comment ça marche ?
Les entités XML sont un moyen de définir des raccourcis pour des fragments de données. Une "entité externe" peut référencer une ressource externe via un URI. Si le parseur XML de l'application est configuré pour résoudre les entités externes, un attaquant peut manipuler ce mécanisme.
Exemple de payload XXE (conceptuel) :
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
Si le parseur traite ce XML, il tentera de charger le contenu de /etc/passwd et de l'insérer à la place de &xxe;.
Prévention de l'Injection XXE
La meilleure défense contre les attaques XXE est de désactiver la résolution des entités externes (et des DTD externes) dans le parseur XML. C'est une configuration qui varie selon le langage et la bibliothèque XML utilisée.
- Java : Utilisez
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);et désactivezEXT_ENTITY_PROCESSINGetDTD_PROCESSING. - PHP :
libxml_disable_entity_loader(true); - .NET : Utilisez
XmlReader.Createavec desXmlReaderSettingsoùDtdProcessingest réglé surProhibitouIgnore. - Python : Dans
lxmlouxml.etree.ElementTree, assurez-vous queresolve_entitiesest désactivé ou utilisez des méthodes de parsing sécurisées.
Assurez-vous également de valider et nettoyer toutes les entrées XML si la désactivation complète n'est pas possible.
Injection NoSQL
Avec l'adoption croissante des bases de données NoSQL, les vulnérabilités d'injection NoSQL sont apparues. Similaires aux injections SQL, elles exploitent la construction dynamique de requêtes pour des bases de données comme MongoDB, Cassandra ou CouchDB.
Comment ça marche ?
Le mécanisme exact dépend de la base de données NoSQL. Par exemple, avec MongoDB, les requêtes sont souvent des objets JSON. Si une application construit une requête en injectant des chaînes directement dans des champs de requête, un attaquant peut manipuler la logique de la requête ou injecter des opérateurs de base de données.
Exemple conceptuel de code MongoDB (Node.js/Mongoose) vulnérable :
// Ne pas faire cela en production !
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Requête MongoDB vulnérable si les entrées sont mal gérées
// Si 'username' est une chaîne qui contient des opérateurs JSON,
// ou si on utilise '$where' ou '$eval' avec l'entrée utilisateur.
const user = await User.findOne({ username: username, password: password });
if (user) {
res.send('Connexion réussie!');
} else {
res.status(401).send('Identifiants invalides.');
}
});
Un attaquant pourrait envoyer un nom d'utilisateur comme {"$gt": ""} ou {"$ne": null} (MongoDB) pour contourner la vérification de mot de passe, en fonction de la manière dont la requête est construite et des opérateurs supportés.
Prévention de l'Injection NoSQL
- Ne jamais construire de requêtes en concaténant des chaînes avec des entrées utilisateur.
- Utilisez toujours les API natives du pilote NoSQL qui gèrent la sérialisation et l'échappement des données (comme les méthodes de
find,update,insertavec des objets JSON). - Évitez l'utilisation de
$whereou$eval(opérateurs qui exécutent du code JavaScript côté serveur dans MongoDB) avec des entrées utilisateur, à moins d'une validation et d'une désinfection extrêmes. - Validez rigoureusement les types de données des entrées utilisateur (par exemple, un ID doit être un entier, un nom d'utilisateur une chaîne alphanumérique).
Principes de Prévention Générale des Injections
Au-delà des techniques spécifiques à chaque type d'injection, voici des principes fondamentaux pour construire des applications résilientes :
-
Ne Jamais Faire Confiance aux Entrées Utilisateur (Principle of Least Trust)
Toute donnée provenant de l'extérieur de votre application (paramètres URL, corps de requête, en-têtes HTTP, cookies, données de fichiers uploadés, etc.) doit être considérée comme potentiellement malveillante.
-
Utiliser des Requêtes Paramétrées / Préparées
La règle d'or pour les bases de données relationnelles (SQL). Séparer le code SQL des données via des marqueurs de position (placeholders) est le moyen le plus efficace de prévenir les injections SQL. S'applique aussi conceptuellement aux NoSQL.
-
Validation des Entrées (Input Validation)
- Validation par liste blanche (Whitelist Validation) : C'est la méthode la plus sûre. Définissez exactement ce qui est autorisé (type de données, format, plage de valeurs, caractères acceptés) et rejetez tout ce qui ne correspond pas. Par exemple, si vous attendez un ID numérique, vérifiez qu'il s'agit bien d'un nombre entier.
- Évitez la validation par liste noire (Blacklist Validation) : Tenter de bloquer tous les caractères ou motifs malveillants est extrêmement difficile et souvent incomplet, car les attaquants trouvent toujours des moyens de contourner.
-
Échappement des Sorties (Output Encoding)
Bien que plus directement liée à l'XSS, l'échappement des sorties est crucial lorsque des données non fiables sont affichées ou utilisées dans un nouveau contexte (HTML, JavaScript, URL, etc.). Le but est de s'assurer que les données ne sont pas interprétées comme du code dans le nouveau contexte.
-
Utiliser des API Sécurisées
- Privilégiez les fonctions et les bibliothèques qui offrent des mécanismes de sécurité intégrés pour interagir avec des systèmes externes (bases de données, système d'exploitation, XML parsers).
- Évitez les fonctions qui exécutent du code dynamique (
eval(),exec(),shell_exec(), etc.) avec des entrées utilisateur.
-
Principe du Moindre Privilège (Principle of Least Privilege)
- Bases de données : Donnez aux utilisateurs de la base de données (ceux que votre application utilise) uniquement les permissions nécessaires à leur fonction (ex:
SELECT,INSERT,UPDATEsur des tables spécifiques, pasDROP TABLEouGRANT). - Système d'exploitation : Exécutez l'application avec l'utilisateur système ayant le moins de privilèges possible.
- Bases de données : Donnez aux utilisateurs de la base de données (ceux que votre application utilise) uniquement les permissions nécessaires à leur fonction (ex:
-
Mise à Jour et Gestion des Dépendances
Maintenez vos frameworks, bibliothèques et tous les composants logiciels à jour. Les vulnérabilités d'injection peuvent résider dans des dépendances tierces, et des mises à jour régulières corrigent souvent ces failles.
-
Surveillance et Journalisation
Mettez en place une journalisation robuste des événements de sécurité et surveillez-les. Les tentatives d'injection peuvent souvent être détectées par des anomalies dans les logs (ex: caractères spéciaux dans les requêtes, erreurs SQL inhabituelles).
Conclusion
Les attaques par injection représentent une menace persistante et grave pour la sécurité des applications web et API. Leur efficacité réside dans la capacité des attaquants à manipuler le flux d'exécution d'une application en exploitant une mauvaise séparation entre les données et le code.
Cependant, en adoptant des pratiques de développement sécurisées, notamment l'utilisation systématique des requêtes préparées pour les bases de données, une validation d'entrée rigoureuse basée sur des listes blanches, et l'emploi d'API sécurisées, vous pouvez significativement réduire la surface d'attaque de vos projets.
La sécurité est un processus continu, pas un événement ponctuel. En intégrant ces principes de prévention dans chaque étape du cycle de vie de développement de vos applications, vous construirez des systèmes plus robustes et plus résistants face aux cybermenaces. N'oubliez jamais : les entrées utilisateur sont coupables jusqu'à preuve du contraire.