Injection SQL (SQLi) : fonctionnement, techniques d’exploitation et bonnes pratiques sécurité

Injection SQL (SQLi) : fonctionnement, techniques d'exploitation et prévention

L’injection SQL (SQLi) est l’une des vulnérabilités les plus connues des applications web. Elle reste pourtant pertinente dans les environnements modernes. Même si les frameworks, ORM et bibliothèques d’accès aux données ont considérablement réduit le nombre de requêtes SQL construites manuellement, ils n’ont pas supprimé le risque pour autant.

C’est précisément parce que cette menace reste sous-estimée qu’il est utile d’y revenir en détail. Dans cet article, nous explorons les principes fondamentaux des injections SQL ainsi que les techniques d’exploitation. Nous détaillons également les différents types d’injection SQL, des exemples concrets d’exploitation et les stratégies de prévention à mettre en place.

Guide complet sur les injections SQL (SQLi)

Qu’est-ce qu’une injection SQL ?

Définition et principes d’une injection SQL

SQL, pour Structured Query Language, est le langage utilisé pour interagir avec de nombreux systèmes de gestion de base de données relationnelles. MySQL, MariaDB, PostgreSQL, Microsoft SQL Server, Oracle Database ou SQLite implémentent tous SQL, avec des différences de syntaxe, de fonctions et d’architecture.

Les applications s’appuient sur ces bases pour stocker des comptes utilisateurs, des informations métier, des contenus, des commandes, des droits d’accès, des données de facturation, des configurations ou encore des secrets nécessaires à leur fonctionnement.

Lorsqu’un utilisateur consulte une ressource, effectue une recherche, génère un rapport ou se connecte, le backend doit fréquemment lire ou modifier ces données.

Une requête légitime peut par exemple être :

SELECT id, username, role
FROM users
WHERE username = 'john';

La structure de la requête est définie par le développeur. La valeur john représente une donnée.

Une injection SQL apparaît lorsque cette séparation disparaît et qu’une entrée contrôlée par l’utilisateur peut modifier la syntaxe envoyée au moteur SQL.

L’idée centrale est donc moins de savoir si l’entrée contient des caractères spéciaux que de déterminer si l’application impose une frontière robuste entre le code SQL et les données. Une apostrophe est parfaitement légitime dans une donnée comme O'Connor. Elle devient dangereuse uniquement lorsqu’elle est introduite dans une requête au moyen d’une concaténation qui lui permet de changer le contexte syntaxique.

Comment une requête devient vulnérable à une injection SQL

Considérons une implémentation PHP simplifiée :

$username = $_GET['username'];

$query = "SELECT id, username, role
          FROM users
          WHERE username = '" . $username . "'";

Avec une valeur normale, par exemple john, la requête obtenue correspond à l’intention du développeur :

SELECT id, username, role
FROM users
WHERE username = 'john';

Mais le SGBD ne connaît pas l’origine des différents caractères de cette chaîne. Il ne sait pas qu’une partie provient du code et qu’une autre provient d’une requête HTTP.

Si l’utilisateur est en mesure de fermer la chaîne et d’introduire une nouvelle expression, le moteur analysera l’ensemble comme une seule instruction SQL.

Par exemple, une entrée comme :

' OR '1'='1

peut alors conduire à :

SELECT id, username, role
FROM users
WHERE username = '' OR '1'='1';

La condition ajoutée est vraie pour toutes les lignes. Le comportement de la requête a donc été modifié par une donnée qui aurait dû rester une simple valeur.

La même vulnérabilité peut exister sans apostrophe. Dans un contexte numérique :

$query = "SELECT * FROM products WHERE id = " . $_GET['id'];

l’entrée :

42 OR 1=1

peut directement produire :

SELECT * FROM products WHERE id = 42 OR 1=1;

Ces deux exemples illustrent pourquoi un payload SQLi n’est jamais universel. L’auditeur doit d’abord comprendre le contexte exact de l’entrée.

Quels peuvent être les impacts d’une SQLi ?

L’impact d’une injection SQL varie considérablement. Dans un cas relativement limité, l’attaquant peut modifier un filtre pour afficher des données supplémentaires. Dans d’autres situations, il peut contourner une logique d’authentification, lire des données appartenant à d’autres comptes ou accéder à des informations qui n’étaient jamais destinées à être exposées par l’application.

Lorsque la requête vulnérable autorise des opérations d’écriture, la SQLi peut permettre de modifier ou de supprimer des données. Des droits excessifs peuvent encore élargir l’impact : accès à des tables d’administration, fonctions de gestion du SGBD, lecture ou écriture de fichiers, invocation de procédures privilégiées, voire interaction avec le système d’exploitation.

Il est toutefois important de raisonner avec précision. L’existence d’une SQLi ne signifie pas automatiquement que toutes ces actions sont réalisables. Un compte applicatif strictement limité en lecture sur quelques vues ne présente pas les mêmes risques qu’un compte disposant de privilèges administratifs. Un SGBD hébergé dans un service managé n’expose pas les mêmes primitives qu’une instance ancienne installée avec une configuration permissive sur le même serveur que l’application web.

Le rôle d’un pentest est précisément d’établir cette différence entre impact théorique et impact démontrable.

Injection SQL et injection NoSQL : quelles différences ?

Les injections SQL ciblent les systèmes relationnels et leur langage de requête. Les bases non relationnelles, par exemple MongoDB, peuvent exposer d’autres formes d’injection lorsque des entrées utilisateurs modifient la structure d’un filtre, d’un document de requête ou d’une expression interprétée par le backend.

Une injection NoSQL n’utilise donc pas nécessairement des apostrophes, des opérateurs UNION ou des commentaires SQL. La syntaxe dépend de la technologie et de l’API utilisée. Les deux familles partagent néanmoins une cause fondamentale : une donnée non fiable est autorisée à influencer la logique d’une opération destinée au moteur de données.

Cette proximité conceptuelle ne doit pas conduire à les confondre. Les méthodes de détection, les techniques d’exploitation, la syntaxe et les mesures de prévention spécifiques diffèrent.

Où rechercher des injections SQL ?

Une SQLi peut apparaître partout où une donnée contrôlable influence une opération SQL. Se limiter aux formulaires de connexion ou aux paramètres id laisse une partie importante de la surface d’attaque hors du périmètre.

Paramètres d’URL et formulaires

Les paramètres de query string sont faciles à modifier et constituent des points de test évidents :

GET /product?id=42 HTTP/1.1
Host: application.example

Mais les données issues d’un formulaire POST, d’un chemin comme /users/42, d’un formulaire multipart ou de paramètres cachés sont tout aussi intéressantes. Le nom du paramètre peut donner un indice fonctionnel, mais ne prouve jamais qu’il atteint une base de données.

L’auditeur observe donc le comportement.

  • Un changement de valeur modifie-t-il le nombre de résultats ?
  • Une valeur inexistante produit-elle une réponse différente ?
  • Le champ semble-t-il contrôler un filtre, une recherche, un tri ou une sélection d’objet ?

Ces indices aident à prioriser les tests.

Recherches, filtres, tris et tableaux de bord

Les moteurs de recherche produisent souvent des requêtes dynamiques complexes. Une page peut combiner un mot-clé, une catégorie, une plage de prix, un statut, un tri et une pagination. Chaque option peut être implémentée différemment.

Une requête pourrait être :

SELECT id, name, price
FROM products
WHERE name LIKE '%$search%'
AND category = '$category'
ORDER BY $sort
LIMIT $limit;

search et category sont des valeurs. sort est un élément structurel. limit peut être une expression numérique dont la possibilité de paramétrisation dépend du driver et de la construction choisie. Une même fonctionnalité peut donc présenter plusieurs contextes d’injection distincts.

Les interfaces de reporting, d’export CSV ou PDF et les tableaux de bord administratifs méritent une attention particulière. Ils combinent fréquemment plusieurs filtres, agrégations et tris, parfois implémentés avec du SQL brut pour des raisons de performance ou de flexibilité.

API et corps JSON ou XML

Une API recevant :

{
  "category": "laptop",
  "minPrice": 500,
  "sort": "price"
}

n’est pas protégée par le simple fait d’utiliser JSON. Si le backend concatène category, minPrice ou sort dans une requête, le même risque s’applique.

Les API peuvent même élargir la surface de test parce qu’elles exposent des objets riches : filtres imbriqués, tableaux d’identifiants, paramètres de tri multiples, recherches avancées, champs optionnels et opérations batch. Le format de transport n’est qu’une couche. Ce qui importe est la transformation effectuée entre l’entrée structurée et la requête envoyée au SGBD.

Le même raisonnement vaut pour XML. Les contrôles destinés aux injections SQL doivent être appliqués au moment de la construction de la requête, pas seulement au moment du parsing du document.

GraphQL et resolvers

GraphQL n’est pas une forme de SQL et une requête GraphQL n’est pas automatiquement transformée en requête SQL. Le risque apparaît lorsque le resolver utilise des arguments contrôlés par le client pour construire une opération sur une base relationnelle.

Considérons par exemple :

query {
  products(category: "laptop") {
    id
    name
    price
  }
}

Le resolver reçoit category. S’il appelle un ORM correctement, la valeur sera généralement paramétrée. S’il assemble une raw query pour gérer un filtre complexe, il peut réintroduire une SQLi.

La chaîne à analyser est donc la suivante : la valeur GraphQL entre dans un resolver, traverse éventuellement plusieurs services ou helpers puis atteint un sink SQL. C’est au niveau de ce sink et de la manière dont la requête est construite que se situe la vulnérabilité.

Headers HTTP et cookies

Des applications enregistrent ou traitent en base le User-Agent, le Referer, X-Forwarded-For, des headers métier ou des identifiants de tracking. Un backend peut également utiliser un cookie pour retrouver une session, une préférence ou une campagne.

Ces valeurs sont contrôlables par le client. Un attaquant peut envoyer ses propres headers et modifier ses cookies, y compris lorsque le navigateur les génère habituellement automatiquement. Ils doivent donc être traités comme des entrées non fiables.

Les SQLi basées sur des headers apparaissent notamment dans des composants de journalisation ou d’analytics développés rapidement et considérés à tort comme internes. Elles peuvent être difficiles à détecter parce que le point d’entrée HTTP et le traitement SQL sont éloignés dans l’architecture.

Données stockées et scénarios de second ordre

Une donnée peut être introduite de manière sûre dans la base puis être réutilisée de manière dangereuse. Par exemple, l’inscription utilise :

INSERT INTO companies(name) VALUES (?);

La valeur est correctement paramétrée. Plus tard, un outil de reporting récupère name et construit :

$sql = "SELECT * FROM invoices
        WHERE company_name = '" . $storedName . "'";

La base de données est ici traitée comme une source de confiance alors qu’elle contient une donnée qui provient initialement de l’utilisateur. La vulnérabilité n’existe pas au moment de l’enregistrement ; elle apparaît au moment de la réutilisation.

Ce scénario montre pourquoi la sécurité d’une donnée n’est pas une propriété absolue. Une donnée sûre dans un contexte peut devenir dangereuse dans un autre si elle est interprétée différemment.

Comment détecter une injection SQL ?

Tester une SQLi efficacement ne consiste pas à envoyer une longue liste de payloads au hasard. L’objectif est de construire progressivement une hypothèse sur la requête, d’obtenir un signal reproductible puis de choisir la technique la plus adaptée.

Cartographier les entrées contrôlables

La première étape consiste à inventorier les données susceptibles d’atteindre le backend : paramètres d’URL, segments de chemin, corps de formulaire, JSON, XML, cookies, headers, champs stockés, filtres, tris et paramètres de pagination.

Dans un contexte white box, cette cartographie peut être complétée par une analyse des sources et des sinks SQL dans le code.

L’auditeur ne teste pas nécessairement toutes les entrées avec la même intensité. Les paramètres qui pilotent une recherche, une sélection d’objet, un export ou une requête de reporting ont souvent plus de chances d’interagir avec une couche de données que des valeurs purement présentées côté client.

Établir une réponse de référence

Avant de modifier une requête, il est utile de mesurer son comportement normal : code HTTP, taille approximative, contenu caractéristique, nombre de résultats, temps de réponse, redirections et éléments JSON importants.

Considérons :

GET /products?id=42 HTTP/1.1
Host: application.example

La réponse de référence permet de comparer ensuite des variations très proches. Sans baseline, une différence observée après un payload peut être attribuée à tort à la SQLi alors qu’elle provient d’un cache, d’une donnée absente ou d’un comportement métier normal.

Rechercher une rupture de syntaxe

Dans un contexte supposé textuel, un caractère comme ' peut provoquer une erreur. Dans un contexte numérique, un opérateur inattendu peut avoir un effet différent. L’objectif de cette étape n’est pas de conclure immédiatement, mais de déterminer si la syntaxe de l’entrée semble atteindre un interpréteur SQL.

Un changement de code HTTP, une exception, un nombre de résultats différent ou une page vide peuvent constituer des indices. Une stack trace mentionnant un driver SQL est particulièrement informative, mais une application bien configurée ne devrait pas exposer ce niveau de détail.

Il faut ensuite confirmer l’hypothèse avec des tests qui produisent des résultats prévisibles.

Comparer des conditions vraies et fausses

Dans un contexte numérique, une paire simple peut être :

42 AND 1=1

puis :

42 AND 1=2

Si la première réponse reproduit de façon stable le comportement normal et que la seconde produit un résultat différent, l’hypothèse d’une injection devient forte. Le même principe peut être adapté à un contexte de chaîne en respectant la syntaxe de la requête supposée.

Cette méthode est particulièrement intéressante parce qu’elle ne dépend pas nécessairement d’une erreur SQL visible. Elle démontre qu’une condition introduite par l’utilisateur influence directement le résultat de la requête.

Rechercher un canal temporel

Si aucune différence de contenu n’est visible, l’auditeur peut rechercher un délai contrôlé. MySQL et MariaDB disposent par exemple de SLEEP(), PostgreSQL de pg_sleep() et SQL Server de WAITFOR DELAY.

Une requête lente ne constitue pas une preuve. Le temps de réponse dépend du réseau, de la charge serveur, de services tiers et de nombreux autres facteurs. Le test doit donc comparer plusieurs requêtes de contrôle et plusieurs requêtes qui devraient provoquer un délai nettement supérieur au bruit normal.

L’étape suivante consiste à rendre ce délai conditionnel. Si le serveur attend uniquement lorsque l’expression SQL est vraie, le temps devient un oracle qui peut être utilisé pour extraire de l’information.

Interpréter correctement les erreurs

Les erreurs peuvent être utiles à plusieurs niveaux. Une erreur de syntaxe suggère un contexte. Une erreur ORDER BY peut aider à déterminer le nombre de colonnes. Une erreur de conversion peut révéler qu’une colonne attend un type numérique.

Il faut distinguer ces erreurs d’une véritable extraction error-based. Dans ce second cas, l’auditeur force le SGBD à produire une erreur dont le message contient une donnée issue d’une sous-requête. L’erreur devient alors un canal d’exfiltration et non un simple indice de structure.

La disponibilité de cette technique dépend fortement du moteur, de sa version et de la façon dont l’application propage ou masque les exceptions.

Confirmer le contexte et le SGBD

Une fois le signal confirmé, l’auditeur cherche à déterminer le moteur sous-jacent. Les fonctions de version, les messages d’erreur, la syntaxe des commentaires, les fonctions de temporisation et les comportements face à certaines constructions constituent des indices.

Le fingerprinting doit éviter les conclusions hâtives. @@version existe par exemple sur plusieurs moteurs. Un seul test positif peut donc ne pas suffire à distinguer MySQL de SQL Server. L’idéal est de croiser plusieurs primitives ou de récupérer une chaîne de version identifiable lorsque le canal de sortie le permet.

Cas pratique d’analyse d’une SQLi

Considérons une application de catalogue qui expose :

GET /products?category=laptops HTTP/1.1
Host: shop.example
Cookie: session=...

La page retourne une liste de produits appartenant à la catégorie laptops. L’auditeur ne connaît ni la requête SQL, ni le nombre de colonnes, ni le SGBD. Il dispose uniquement du comportement HTTP.

Identification du contexte et confirmation de la vulnérabilité d’injection SQL

La première étape consiste à établir la réponse de référence : statut 200, longueur de la réponse, nombre de produits affichés et quelques chaînes stables.

L’auditeur remplace ensuite la catégorie par une valeur inexistante afin de vérifier le comportement attendu lorsque la requête ne retourne aucune ligne.

Il teste enfin une apostrophe isolée :

laptops'

Supposons que l’application renvoie cette fois une erreur générique avec un statut 500. Ce signal est intéressant mais insuffisant.

L’auditeur cherche donc à construire deux requêtes qui ne diffèrent que par une condition logique. Dans le contexte supposé d’une chaîne, il adapte ses valeurs afin que la syntaxe reste valide et compare une condition vraie avec une condition fausse.

La condition vraie reproduit la liste normale alors que la condition fausse retourne une liste vide. Après plusieurs répétitions, le comportement reste stable. La vulnérabilité est désormais beaucoup plus crédible : une expression SQL introduite par l’utilisateur modifie le résultat de la requête.

Cette étape est essentielle. Elle évite d’interpréter une erreur ponctuelle comme une SQLi et fournit une méthode qui pourra rester utile si les techniques directes échouent.

Choix du canal d’exploitation

L’auditeur cherche ensuite à savoir si le résultat de la requête est directement affiché. Puisque la page présente des produits, une attaque UNION est un candidat naturel.

Il teste progressivement ORDER BY 1, ORDER BY 2 et les positions suivantes. Les trois premières positions sont acceptées, tandis que la quatrième provoque une réponse d’erreur. La requête originale semble donc retourner trois colonnes.

Il confirme cette hypothèse avec une construction comportant trois valeurs NULL. La requête est acceptée. En remplaçant ensuite chaque NULL par une chaîne de test, il observe que la deuxième et la troisième positions acceptent du texte, mais que seule la deuxième apparaît dans le HTML.

À ce stade, plusieurs éléments sont établis : le point est injectable, l’injection se trouve vraisemblablement dans une chaîne, la requête retourne trois colonnes et la deuxième colonne constitue un canal d’affichage exploitable.

L’étape suivante consiste à identifier le SGBD. Un test avec une fonction spécifique retourne une chaîne de version PostgreSQL. L’auditeur peut alors utiliser la syntaxe PostgreSQL pour les étapes suivantes plutôt que d’essayer aveuglément des fonctions MySQL, SQL Server ou Oracle.

Il récupère le nom de la base courante et quelques noms de tables visibles via les métadonnées. Une table métier contenant des comptes utilisateurs apparaît. L’auditeur n’a pas besoin d’en extraire le contenu complet : il cherche plutôt à déterminer quelles colonnes existent et si des informations sensibles sont réellement accessibles au compte applicatif.

Supposons qu’il identifie id, email, password_hash et role. La présence de password_hash montre déjà que le compte SQL utilisé par le catalogue peut accéder à une donnée qui dépasse vraisemblablement le besoin fonctionnel d’une page publique de produits. Un enregistrement de test ou une valeur appartenant à un compte de démonstration peut suffire à prouver la lecture non autorisée.

Le test révèle alors deux problèmes distincts : la SQLi elle-même et une séparation de privilèges insuffisante, puisque le compte utilisé par le composant catalogue peut lire une table d’authentification.

Si UNION avait échoué parce que les résultats n’étaient pas affichés, l’auditeur aurait pu revenir à la méthode booléenne déjà confirmé. Si le contenu avait été strictement identique, un canal temporel aurait pu être recherché.

La méthodologie consiste donc à privilégier le canal le plus direct et le moins coûteux, puis à basculer vers des techniques blind uniquement lorsque c’est nécessaire.

Évaluation d’impact de la SQLi

Une fois la lecture de données sensibles démontrée, il serait techniquement possible de poursuivre l’énumération. Cela ne signifie pas que ce soit pertinent. Un pentest doit produire suffisamment de preuves pour permettre la qualification du risque et la remédiation, pas maximiser la quantité de données exfiltrées.

L’auditeur examine plutôt les privilèges du compte. Peut-il écrire ? Peut-il créer des objets ? Dispose-t-il de fonctions privilégiées ?

Dans notre scénario, supposons que le rôle soit limité à SELECT sur plusieurs schémas. L’impact demeure élevé parce que des données sensibles sont accessibles, mais la probabilité d’une modification de la base ou d’une RCE directe est fortement réduite.

Cette conclusion est plus utile qu’une affirmation générique selon laquelle « une SQLi peut mener à une RCE ». Le rapport peut expliquer que la vulnérabilité permet une extraction arbitraire des données lisibles par le compte, que ce compte a un périmètre excessif au regard de la fonction publique testée, mais qu’aucune possibilité d’écriture ou d’exécution système n’a été identifiée dans le contexte autorisé.

Le scénario fournit également une remédiation plus précise. Il faut paramétrer la valeur category dans la requête, mais aussi revoir le compte utilisé par le catalogue afin qu’il ne puisse accéder qu’aux tables ou vues strictement nécessaires. La correction du code supprime la cause racine ; le moindre privilège réduit l’impact d’une éventuelle vulnérabilité future.

Lors du contre-test, l’auditeur rejouera la condition vraie et la condition fausse, vérifiera que les chaînes contenant des caractères SQL sont traitées comme de simples valeurs, puis confirmera que le compte ou la couche de données n’expose plus inutilement les objets d’authentification.

Ce cas pratique résume le raisonnement à conserver tout au long de ce guide : confirmer avant d’exploiter, identifier le contexte avant de choisir les payloads, utiliser le canal le plus simple, limiter l’accès aux données au strict nécessaire et qualifier l’impact à partir des capacités réellement démontrées.

Types et techniques d’exploitation des injections SQL

Les catégories de SQLi décrivent surtout le canal utilisé pour obtenir une information ou étendre l’impact. Une même vulnérabilité peut parfois être exploitable de plusieurs façons.

Lorsque les résultats sont directement affichés, une technique in-band est généralement plus efficace qu’une extraction blind. Lorsque l’application ne renvoie rien d’utile, un booléen, temporel ou out-of-band devient nécessaire.

In-band SQL Injection

On parle d’in-band SQL injection lorsque le même canal applicatif sert à envoyer la charge utile et à récupérer les données ou indices utiles.

Les deux familles les plus représentatives sont les injections UNION-based et error-based.

UNION-based SQL Injection

L’opérateur UNION permet de combiner les résultats de plusieurs requêtes SELECT. Une requête légitime comme :

SELECT name, price
FROM products;

peut être combinée avec :

SELECT username, email
FROM users;

à condition que les deux résultats aient le même nombre de colonnes et que les types correspondants soient compatibles.

Dans une SQLi, l’objectif est d’utiliser ce mécanisme pour faire traiter le résultat d’une requête contrôlée comme s’il appartenait au résultat original.

Supposons qu’une fonctionnalité de catégorie construise :

SELECT name, description, price
FROM products
WHERE category = '$category';

L’auditeur ne peut pas commencer efficacement par un UNION SELECT arbitraire. Il doit d’abord déterminer le nombre de colonnes, les positions compatibles avec du texte et celles réellement affichées dans la réponse.

Déterminer le nombre de colonnes avec ORDER BY

Dans de nombreux SGBD, ORDER BY 1, ORDER BY 2 ou ORDER BY 3 permet de désigner les colonnes du résultat par leur position. L’auditeur peut augmenter progressivement cette valeur :

' ORDER BY 1 --
' ORDER BY 2 --
' ORDER BY 3 --
' ORDER BY 4 --

Lorsque la position dépasse le nombre de colonnes, le moteur peut générer une erreur. L’application n’affiche pas nécessairement le message SQL ; une réponse générique différente, un statut 500 ou l’absence de résultat peuvent suffire à identifier le seuil.

Si ORDER BY 3 reste valide mais ORDER BY 4 provoque systématiquement un comportement différent, la requête originale retourne vraisemblablement trois colonnes.

Cette technique doit être adaptée au contexte. Le commentaire utilisé après le payload dépend notamment du SGBD et de la syntaxe attendue. Sur MySQL, la séquence -- doit être suivie d’un caractère d’espacement valide ; le caractère # est également supporté comme commentaire de fin de ligne.

Déterminer le nombre de colonnes avec UNION SELECT NULL

Une autre méthode consiste à augmenter le nombre de valeurs NULL jusqu’à obtenir une requête compatible :

UNION SELECT NULL

puis :

UNION SELECT NULL,NULL

et ainsi de suite.

NULL est utile parce qu’il peut être converti vers de nombreux types SQL. Il maximise donc les chances que la requête réussisse une fois le bon nombre de colonnes atteint, sans connaître encore le type de chaque colonne.

Une erreur avec deux valeurs puis un succès avec trois suggère que le résultat original comporte trois colonnes.

Identifier les colonnes compatibles avec du texte

Le nombre de colonnes ne suffit pas. Si l’auditeur souhaite récupérer un nom d’utilisateur, une version de base ou une chaîne de métadonnées, il doit trouver une colonne qui accepte des données textuelles.

Pour un résultat à trois colonnes, il peut tester successivement :

UNION SELECT 'test',NULL,NULL
UNION SELECT NULL,'test',NULL
UNION SELECT NULL,NULL,'test'

Une erreur de conversion peut indiquer que la position testée correspond à un entier, une date ou un autre type incompatible. Si la requête réussit, la colonne est probablement compatible avec une chaîne.

Cette étape doit être distinguée d’une seconde question : la colonne est-elle réellement visible ? Une application peut récupérer quatre colonnes puis n’en utiliser que deux dans son template HTML ou sa réponse JSON. Une position peut donc être techniquement compatible sans constituer un canal d’exfiltration exploitable.

Identifier les colonnes réellement affichées

Une technique simple consiste à injecter une chaîne reconnaissable dans chaque colonne compatible et à vérifier où elle réapparaît dans la réponse. Si HACKAGORA_TEST_47 apparaît dans un titre de produit, un champ JSON ou un attribut HTML, cette position fournit un canal de sortie.

Il arrive que plusieurs résultats légitimes masquent la ligne ajoutée par le UNION, ou que l’application ne traite que le premier enregistrement. Dans ce cas, l’auditeur peut chercher à rendre la partie originale de la requête fausse afin de ne conserver que les résultats de sa seconde sélection. Cette adaptation dépend du contexte et doit rester non destructive.

Fingerprinting au moyen d’un UNION

Une fois une colonne textuelle visible identifiée, le canal peut servir à reconnaître le SGBD. Selon le moteur, des expressions comme celles-ci sont pertinentes :

  • SELECT @@version; pour MySQL ou SQL Server,
  • SELECT version(); pour PostgreSQL,
  • ou l’interrogation de v$version pour Oracle.

Dans un contexte UNION, ces expressions doivent être insérées dans le bon nombre de colonnes et à une position compatible. L’obtention d’une chaîne de version permet ensuite d’adapter les fonctions, les commentaires, la concaténation et les techniques d’énumération.

Énumérer les métadonnées avec UNION

De nombreux moteurs exposent des métadonnées via information_schema. Lorsque les permissions le permettent, une requête comme :

SELECT table_name
FROM information_schema.tables;

peut révéler des tables. L’auditeur peut ensuite examiner les colonnes d’un objet intéressant :

SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users';

L’objectif n’est pas nécessairement d’extraire toutes les données. En pentest, quelques éléments représentatifs suffisent souvent à démontrer qu’un compte SQL peut accéder à une table sensible qui n’aurait jamais dû être exposée via la fonctionnalité vulnérable.

Concaténer plusieurs valeurs dans une seule colonne

Une interface peut n’afficher qu’une seule colonne textuelle. L’auditeur peut alors concaténer plusieurs champs pour les récupérer ensemble. MySQL propose notamment :

CONCAT(username, ':', email)

PostgreSQL et Oracle utilisent couramment :

username || ':' || email

SQL Server peut utiliser l’opérateur + ou d’autres fonctions de concaténation selon les types et la version.

Cette différence montre encore une fois pourquoi l’étape de fingerprinting n’est pas simplement cosmétique. Elle permet de choisir des méthodes compatibles avec le moteur réel.

Error-based SQL Injection

Le terme error-based est souvent utilisé pour toute exploitation qui profite d’une erreur SQL. Il est utile de distinguer les erreurs qui aident à comprendre la requête de celles qui servent réellement à extraire une donnée.

Une erreur provoquée par :

ORDER BY 10

peut révéler que la requête comporte moins de dix colonnes. Une conversion impossible peut indiquer le type attendu. Un message de syntaxe peut révéler le moteur. Ces informations facilitent l’exploitation, mais la donnée recherchée n’est pas encore transportée par l’erreur.

Dans une error-based SQLi au sens plus strict, l’auditeur force le SGBD à effectuer une opération invalide dont le message incorpore une valeur provenant d’une sous-requête. Le mécanisme conceptuel est le suivant : la base calcule une information, cette information est utilisée dans un contexte provoquant volontairement une exception, puis le message est renvoyé à l’application.

Une erreur du type :

Cannot convert value 'production_database' ...

peut ainsi révéler production_database alors que la fonctionnalité n’affiche jamais le résultat SQL directement.

Les techniques précises varient fortement entre moteurs et versions. Elles peuvent également être rendues inutilisables par une gestion des erreurs qui remplace les exceptions SQL par un message générique. Cette mesure réduit fortement la quantité d’information disponible, mais ne corrige évidemment pas la SQLi elle-même.

Blind SQL Injection

Une injection blind existe lorsque la requête est manipulable mais que l’application ne renvoie ni le résultat SQL ni, dans la plupart des cas, un message d’erreur directement exploitable. L’attaquant doit alors déduire les informations en observant un effet secondaire stable.

Les deux canaux les plus courants sont les différences de contenu ou de comportement et le temps de réponse.

Boolean-based Blind SQL Injection

Supposons qu’un endpoint retourne Produit disponible lorsque sa requête trouve une ligne et Produit introuvable dans le cas contraire.

Une entrée :

42 AND 1=1

conserve le résultat normal, tandis que :

42 AND 1=2

fait disparaître le produit. L’application vient de fournir un indice : elle répond indirectement à une question SQL par vrai ou faux.

L’étape suivante consiste à remplacer 1=1 par une condition portant sur une donnée réelle. Dans MySQL, par exemple :

LENGTH(DATABASE()) = 10

permet de demander si le nom de la base courante comporte dix caractères. Une fois la longueur déterminée, une expression comme :

SUBSTRING(DATABASE(),1,1) = 'a'

permet de tester la première position.

Une approche naïve essaierait tous les caractères possibles successivement. Cela devient rapidement coûteux. Une extraction blind efficace repose plutôt sur la recherche dichotomique. Au lieu de demander si le caractère vaut a, puis b, puis c, l’auditeur compare sa valeur numérique à un seuil. Chaque réponse élimine approximativement la moitié des possibilités.

Avec un espace de 128 valeurs, sept questions suffisent théoriquement à isoler une valeur puisque 2^7 = 128. Les outils automatisés utilisent ce type d’optimisation pour réduire fortement le nombre de requêtes nécessaires.

Dans une application réelle, le vrai et le faux ne sont pas toujours représentés par des messages explicites. Le signal peut être un champ JSON, une différence de nombre de résultats, une redirection, un changement de longueur de quelques octets ou la présence d’un élément HTML. L’auditeur doit vérifier que ce signal est reproductible et qu’il ne dépend pas d’une donnée volatile, d’un cache ou d’un autre mécanisme métier.

Time-based Blind SQL Injection

Lorsque le contenu de la réponse ne varie pas, l’auditeur peut utiliser le temps comme oracle. Les primitives sont spécifiques au moteur :

  • SLEEP(5) sur MySQL ou MariaDB,
  • pg_sleep(5) sur PostgreSQL,
  • et WAITFOR DELAY '0:0:5'; sur SQL Server.

Provoquer un délai fixe démontre surtout qu’une fonction de temporisation est atteignable. Pour extraire des données, il faut rendre cette temporisation conditionnelle. La logique devient : si une expression est vraie, attendre ; sinon, répondre normalement.

L’auditeur peut ainsi tester la longueur d’une chaîne, puis chacun de ses caractères. La recherche dichotomique reste pertinente : le délai est simplement le support utilisé pour représenter le bit vrai ou faux.

Le principal défi est le bruit. Un serveur peut répondre en 200 ms puis 900 ms sans qu’aucune SQLi ne soit impliquée. Les tests doivent donc utiliser un délai nettement supérieur à la variance habituelle et être répétés. L’auditeur compare plusieurs requêtes de contrôle et plusieurs requêtes censées déclencher l’attente.

Une exploitation time-based peut également produire une charge importante, en particulier si chaque question impose plusieurs secondes d’attente. Sur un environnement de production, confirmer que l’oracle permet de tester une donnée interne est souvent suffisant. Extraire de longues chaînes simplement parce que la technique le permet peut être inutilement intrusif.

Out-of-Band SQL Injection

Certaines applications exécutent la requête de manière asynchrone ou neutralisent toutes les différences de contenu et de temps exploitables. Il peut néanmoins être possible d’amener le SGBD à initier une interaction réseau vers une infrastructure contrôlée par l’auditeur.

Une SQLi out-of-band utilise alors un autre canal, typiquement DNS ou HTTP, pour confirmer l’exploitation ou transporter une donnée. Conceptuellement, la base peut être amenée à provoquer une résolution dont le nom contient une valeur extraite :

production-db.audit.example

Le serveur DNS de test reçoit la requête et permet d’observer la donnée sans qu’elle ne transite dans la réponse HTTP de l’application.

La technique dépend de plusieurs prérequis. Le SGBD doit disposer d’une primitive réseau utilisable dans le contexte de l’injection, le compte SQL doit avoir les droits nécessaires et l’environnement doit autoriser le flux sortant. Une segmentation stricte et un filtrage egress peuvent donc bloquer ce canal même si la SQLi elle-même existe.

Cette distinction est importante pour l’analyse d’impact : une primitive documentée dans un moteur ne signifie pas qu’elle est accessible dans l’environnement audité.

Stacked Queries

Une injection UNION ajoute un second SELECT au résultat de la requête originale. Les stacked queries, également appelées batched ou piggy-backed queries, cherchent à terminer l’instruction existante puis à exécuter une nouvelle instruction indépendante.

Prenons par exemple :

SELECT * FROM products WHERE id = 42;
UPDATE audit_marker SET checked = 1 WHERE id = 99999;

La seconde instruction n’est plus limitée à la forme d’un SELECT. Selon les privilèges, elle pourrait être un UPDATE, un appel de procédure ou une autre instruction prise en charge par le moteur.

Le support ne dépend pas uniquement du SGBD. Le driver ou l’API applicative peut interdire plusieurs statements dans un même appel, ou nécessiter une option explicite. Deux applications utilisant la même technologie de base peuvent donc exposer des capacités différentes.

En pentest, la confirmation doit privilégier une opération sans effet métier persistant. Une temporisation contrôlée ou une requête inoffensive est préférable à une modification de données lorsque le simple support des stacked queries suffit à démontrer l’élargissement du périmètre d’exploitation.

Second-Order SQL Injection

L’injection de second ordre apparaît lorsqu’une donnée malveillante est stockée sans provoquer d’effet, puis réutilisée ultérieurement dans une requête vulnérable.

Le point d’entrée et le point d’exécution peuvent être très éloignés. Un nom d’entreprise est enregistré aujourd’hui via une requête paramétrée. Un export interne le récupère demain et le concatène dans un filtre SQL. Le premier composant n’est pas vulnérable ; le second l’est.

Ce scénario est particulièrement difficile à détecter par un scanner dynamique classique. La réponse à la requête d’enregistrement est parfaitement normale et aucune erreur n’apparaît. Il faut comprendre le cycle de vie de la donnée, identifier les fonctionnalités qui la relisent et tester les sinks ultérieurs.

Les interfaces d’administration, rapports, exports, tâches planifiées, traitements batch et synchronisations sont des emplacements fréquents pour ce type de réutilisation. Les développeurs y considèrent parfois à tort les données de la base comme implicitement fiables.

La règle de sécurité doit être formulée autrement : une donnée doit être utilisée de manière sûre dans chaque contexte où elle est interprétée, quelle que soit sa provenance immédiate.

Techniques avancées d’exploitation d’injection SQL

Au-delà du scénario précédent, certaines techniques permettent d’étendre l’accès bien au-delà du point d’injection initial. Tout dépend alors des privilèges de la base et de l’infrastructure sous-jacente.

Lecture et écriture de fichiers

Certains SGBD exposent des fonctionnalités d’interaction avec le système de fichiers. Si les privilèges accordés à la base sont trop élevés, un attaquant peut en abuser pour lire des fichiers serveur sensibles ou écrire du contenu arbitraire sur le disque.

MySQL expose par exemple la fonction LOAD_FILE() :

SELECT LOAD_FILE('/etc/passwd');

Il est également possible d’écrire des fichiers via :

SELECT 'malicious content' INTO OUTFILE '/var/www/html/shell.php';

SQL Server expose de son côté des procédures stockées tout aussi dangereuses pour interagir avec le système d’exploitation. Cette capacité d’interaction avec le système de fichiers augmente considérablement la gravité d’une injection SQL, puisqu’elle peut donner accès à des fichiers de configuration, des secrets applicatifs, des clés SSH, du code source, des sauvegardes ou des scripts serveur.

Dans les environnements les plus exposés, la capacité d’écriture de fichiers peut mener à une compromission totale du serveur.

Exécution de code à distance (RCE) via injection SQL

Dans certains cas, une injection SQL peut aller au-delà de la simple compromission de la base et mener à de l’exécution de code à distance (RCE) sur le système d’exploitation sous-jacent. Cela survient généralement lorsque :

  • la base de données tourne avec des privilèges excessifs ;
  • des procédures stockées dangereuses sont activées ;
  • l’écriture de fichiers est autorisée ;
  • des fonctions d’exécution de commandes externes sont accessibles.

SQL Server expose par exemple la procédure stockée xp_cmdshell :

EXEC xp_cmdshell 'whoami';

À partir d’une injection SQL, un attaquant peut ainsi déposer un web shell, écrire des scripts malveillants dans un répertoire accessible, déclencher des fonctions système, ou exécuter des binaires externes.

Une RCE réussie peut alors permettre de compromettre entièrement le serveur, établir une persistance, se déplacer latéralement dans le réseau interne, déployer un ransomware, ou exfiltrer des données à grande échelle. Si les pratiques de durcissement actuelles désactivent généralement ces fonctions dangereuses par défaut, les configurations mal sécurisées et les environnements legacy continuent d’exposer ce type de chemin d’attaque à haut risque.

Contournement des WAF

De nombreuses organisations déploient des WAF (Web Application Firewalls) pour détecter et bloquer les payloads d’injection SQL malveillants. Les attaquants tentent cependant régulièrement de les contourner via des techniques d’obfuscation : encodage du payload, manipulation de la casse, commentaires inline, espaces parasites, fragmentation de la syntaxe SQL, ou utilisation d’opérateurs et fonctions alternatifs.

Exemple de payload reformaté pour échapper à une détection par signature :

UN/**/ION SEL/**/ECT

L’encodage URL, voire le double encodage, est également utilisé pour dissimuler l’entrée malveillante avant qu’elle n’atteigne le backend. Comme de nombreux WAF reposent fortement sur le pattern matching et la détection par signature, un jeu de règles mal configuré peut échouer à détecter un payload fortement obfusqué. Un WAF reste un contrôle de défense précieux, mais ne doit jamais être considéré comme un substitut à une gestion sécurisée des requêtes via des requêtes paramétrées.

Élévation de privilèges

La gravité d’une injection SQL dépend en grande partie des privilèges attribués au compte de base de données compromis. Des permissions excessives élargissent considérablement la surface d’attaque et permettent une compromission plus profonde. Un attaquant cherche généralement à élever ses privilèges pour accéder à des tables restreintes, exécuter des fonctions administratives, interagir avec le système de fichiers, compromettre d’autres services, ou se déplacer latéralement dans l’infrastructure.

Une application tournant avec un compte de base de données trop privilégié peut ainsi exposer par inadvertance des procédures stockées administratives, des fonctions de gestion de la base, des capacités d’interaction avec le système d’exploitation, ou des schémas internes sensibles. Dans certains environnements, des identifiants extraits peuvent même servir à compromettre d’autres systèmes connectés à la même infrastructure.

Une mauvaise séparation des privilèges reste l’un des facteurs les plus déterminants dans la gravité d’une exploitation par injection SQL. Même une faille relativement simple peut devenir critique si le compte de base de données sous-jacent dispose de permissions excessives.

Comment prévenir les attaques par injection SQL ?

Prévenir une attaque par injection SQL repose sur une combinaison de bonnes pratiques de développement, d’une configuration sécurisée de la base de données, de privilèges restreints et de tests de sécurité réguliers. Puisque la faille naît d’un manque de séparation entre donnée utilisateur et instruction SQL exécutable, une stratégie de mitigation efficace doit couvrir chaque étape de l’interaction avec la base.

Les frameworks et bibliothèques modernes proposent des mécanismes plus sûrs, mais les implémentations mal sécurisées, le code legacy et les requêtes construites à la main continuent d’exposer les applications.

Utiliser des requêtes préparées

Les requêtes préparées restent la protection la plus efficace contre les injections SQL. Contrairement à une requête construite dynamiquement, elles séparent la structure de la requête de la donnée fournie par l’utilisateur : l’application envoie le gabarit de requête et les paramètres séparément au moteur de base de données, qui traite alors systématiquement l’entrée comme une donnée et jamais comme une instruction.

Exemple de construction non sécurisée :

$query = "SELECT * FROM users WHERE id = " . $_GET['id'];

Version sécurisée avec liaison de paramètres :

$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);

Comme le moteur distingue strictement la logique SQL de la valeur fournie, un payload injecté ne peut plus altérer la structure de la requête. Les requêtes préparées sont largement supportées par les langages, frameworks et drivers de base de données modernes. Il s’agit de défense de base contre les injections SQL.

Mettre en place une validation des entrées

La validation des entrées réduit la surface d’attaque en filtrant les données inattendues ou mal formées avant qu’elles n’atteignent la logique applicative ou la base. Une application devrait valider : le type de données, le format attendu, la longueur, les caractères autorisés et les valeurs acceptées.

Un paramètre numérique ne devrait accepter que des valeurs numériques ; un champ email devrait imposer un format email valide. Une approche par liste blanche (définir ce qui est valide) est généralement plus sûre qu’une approche par liste noire (tenter de bloquer ce qui est dangereux).

Attention toutefois : la validation des entrées ne doit jamais être considérée comme une protection suffisante à elle seule. Un attaquant peut souvent contourner un filtrage trop faible ou une protection basée sur une liste noire. Elle doit donc venir compléter et non remplacer les requêtes préparées et une gestion sécurisée des requêtes.

Éviter la construction dynamique non sécurisée de requêtes

La construction dynamique non sécurisée reste l’une des principales causes d’injection SQL : elle survient dès qu’un développeur génère une requête SQL par concaténation de chaînes, interpolation directe, ou variables non fiables. Exemple typique :

$query = "SELECT * FROM products WHERE category = '" . $category . "'";

Ici, une entrée malveillante peut directement modifier la structure et la logique de la requête générée. À éviter : concaténation directe de chaînes, interpolation SQL brute, fragments SQL assemblés dynamiquement, query builders non sécurisés, ou exécution de SQL fourni par l’utilisateur.

Si la génération dynamique reste nécessaire, privilégiez les requêtes paramétrées, des contrôles de validation stricts et des mécanismes de construction de requête sécurisés.

Sécuriser les procédures stockées et la logique côté base de données

Bien implémentées, les procédures stockées peuvent réduire le risque d’injection en encapsulant la logique métier dans des routines prédéfinies, limitant l’interaction directe entre l’entrée utilisateur et le SQL généré dynamiquement.

Elles ne sont toutefois pas intrinsèquement sécurisées : une procédure qui construit elle-même du SQL dynamique reste vulnérable. C’est notamment le cas avec des instructions comme EXEC(@query) ou sp_executesql, si l’entrée attaquant est intégrée sans paramétrage.

Une procédure stockée sécurisée devrait : utiliser la liaison de paramètres, éviter l’exécution de SQL dynamique non sécurisée, restreindre les privilèges inutiles, et limiter les fonctionnalités administratives exposées. La logique côté base de données doit suivre les mêmes principes de sécurité que le code applicatif.

Appliquer le principe du moindre privilège

L’impact d’une injection SQL dépend en grande partie des privilèges du compte de base de données compromis. Une application ne devrait jamais se connecter avec un compte administrateur, sauf nécessité absolue : le compte utilisé doit posséder uniquement les permissions strictement nécessaires.

Une application qui n’a besoin que d’un accès en lecture ne devrait pas disposer de privilèges sur le système de fichiers, de permissions administratives, de droits de modification de schéma, ou de capacités d’exécution de commandes.

Restreindre les privilèges réduit fortement l’impact potentiel d’une exploitation réussie : même si la requête est compromise, des permissions limitées peuvent empêcher l’élévation de privilèges, l’accès au système de fichiers, la modification de la base, l’exécution de code à distance, ou le mouvement latéral dans l’infrastructure. C’est l’un des moyens les plus efficaces de contenir une exploitation.

Désactiver les fonctionnalités dangereuses de la base de données

De nombreux SGBD exposent des fonctionnalités avancées capables d’interagir avec le système d’exploitation ou le système de fichiers. Si elles restent activées sans nécessité, un attaquant peut en abuser lors de l’exploitation : procédures d’exécution de commandes, fonctions de lecture/écriture de fichiers, communication réseau externe, procédures administratives étendues.

SQL Server expose par exemple xp_cmdshell, tandis que MySQL expose LOAD_FILE(). Si l’application n’en a pas besoin, ces fonctions doivent être désactivées ou fortement restreintes. Réduire la surface d’attaque disponible au niveau de la base limite les possibilités de post-exploitation et atténue significativement la gravité d’une injection SQL.

Mettre en place une gestion sécurisée des erreurs

Des messages d’erreur trop détaillés fournissent à l’attaquant des informations précieuses pendant l’exploitation : type et version de la base, noms de tables, structure des requêtes, chemins de fichiers serveur, logique applicative interne.

Une application ne devrait jamais renvoyer une erreur brute de la base de données à l’utilisateur final. Affichez plutôt un message générique côté externe, et conservez les logs détaillés réservés à la supervision interne. Une erreur du type Unknown column 'username' in 'where clause' ne devrait jamais s’afficher côté client.

Une mauvaise gestion des erreurs facilite directement les attaques error-based en exposant des informations backend que l’attaquant peut exploiter pour affiner ses payloads et cartographier la base. Une journalisation et une supervision sécurisées restent essentielles pour repérer les comportements suspects sans exposer d’information sensible.

Réaliser des tests de sécurité en continu

La prévention de l’injection SQL ne peut pas reposer uniquement sur les bonnes pratiques appliquées en début de projet. Des tests de sécurité réguliers sont indispensables pour détecter les nouvelles vulnérabilités, les changements de code à risque et l’évolution de la surface d’attaque.

Les approches les plus efficaces combinent : audits de sécurité manuels, tests d’intrusion, revues de code sécurisées et scans de vulnérabilités automatisés.

Ces tests doivent aussi couvrir les API, les backends mobiles, les interfaces d’administration, les intégrations tierces et les composants legacy. Intégrer ces tests dans les pipelines CI/CD permet de détecter les vulnérabilités plus tôt dans le cycle de développement et de réduire le risque qu’une faille exploitable atteigne la production.

L’injection SQL restant l’une des vulnérabilités web les plus dangereuses et persistantes, une validation de sécurité continue demeure un pilier essentiel de tout programme de sécurité applicatif moderne.

Conclusion

L’injection SQL (SQLi) reste l’une des vulnérabilités web les plus critiques et les plus répandues. Elle figure encore aujourd’hui dans le Top 10 OWASP et est référencée sous CWE-89, preuve de sa persistance.

Elle naît toujours du même défaut : l’absence de séparation entre la structure d’une requête et la donnée fournie par l’utilisateur. Que l’exploitation passe par une erreur visible, un comportement booléen, un délai de réponse ou un canal réseau externe, le mécanisme de fond demeure identique. C’est pourquoi les requêtes préparées, associées à un principe de moindre privilège strict, restent la défense la plus fiable, ORM ou non.

Il faut néanmoins garder à l’esprit qu’une application peut sembler protégée par un framework moderne tout en restant exposée dès qu’un développeur introduit un fragment de SQL dynamique non paramétré. Dès lors, la prévention ne peut reposer uniquement sur le choix des outils : elle exige une vigilance constante, des tests réguliers et, surtout, une compréhension claire comme celle développée dans ce guide du mécanisme réel derrière chaque technique d’exploitation.