Lignes directrices

Injection SQL

Il est temps de se pencher sur l'injection SQL. Pendant longtemps, elle a été le roi incontesté du Top 10 de l'OWASP, plusieurs années de suite. Malgré son ancienneté (plus de 20 ans), et bien qu'elle ait légèrement perdu sa première place sur cette liste, elle reste une vulnérabilité incroyablement populaire et dangereuse. 

En tant que vulnérabilité de sécurité web, l'injection SQL (SQLi) reste l'une des techniques de piratage les plus courantes utilisées par les attaquants, car elle leur permet de manipuler une base de données et d'en extraire des informations cruciales. Plus inquiétant encore, un attaquant peut se faire passer pour l'administrateur du serveur de base de données et faire des choses vraiment dévastatrices comme détruire des bases de données, manipuler des transactions, divulguer des données et rendre le serveur vulnérable à d'autres problèmes.

Voyons rapidement comment cela se passe

SQL (ou Structured Query Language) est le langage utilisé pour communiquer avec les bases de données relationnelles ; c'est le langage de requête utilisé par les développeurs, les administrateurs de bases de données et les applications pour gérer les énormes quantités de données générées chaque jour.

Au sein d'une application, il existe deux contextes : l'un pour les données, l'autre pour le code. Le contexte du code indique à l'ordinateur ce qu'il doit exécuter et le sépare des données à traiter. L'injection SQL se produit lorsqu'un attaquant saisit des données qui sont traitées par erreur comme du code par l'interpréteur SQL, ce qui lui permet de recueillir des informations précieuses à partir de l'application. 

Effets d'une attaque par injection SQL

Une injection SQL peut être extrêmement dangereuse pour n'importe quelle application web et a été la technique préférée derrière tant de brèches très médiatisées parce qu'elle permet aux attaquants d'avoir un accès non autorisé à des données critiques. Ils peuvent voir un grand nombre d'informations, depuis les noms d'utilisateur et les mots de passe jusqu'aux détails des cartes de crédit et aux numéros d'identification personnelle. 

Après avoir obtenu l'accès à ces données, les attaquants peuvent s'emparer de comptes, réinitialiser des mots de passe, faire des achats en ligne ou commettre d'autres types de fraudes (bien plus graves). 

Mais l'aspect le plus alarmant de SQLi est peut-être le fait qu'un attaquant peut, s'il n'est pas détecté, maintenir une porte dérobée dans le système pendant de longues périodes. Comme vous pouvez l'imaginer, cela conduirait à des violations répétées de données aussi longtemps que cette porte dérobée resterait ouverte. De quoi faire froid dans le dos. 

Prenons quelques exemples pour mieux comprendre ce qu'il en est dans la pratique.

Exemples SQLi

SQLi comprend diverses techniques de vulnérabilité qui permettent de faire face à différentes situations. Vous trouverez ci-dessous quelques-uns des exemples les plus courants de SQLi :

Technique Description
Récupération des données cachées Grâce à cette technique, les attaquants peuvent modifier n'importe quelle requête SQL pour obtenir davantage d'informations de la base de données.
Examen des données Les attaquants peuvent extraire des informations sur la version et la structure d'une base de données, ce qui leur permet d'exploiter d'autres informations. Cette technique peut varier d'une base de données à l'autre.
Attaques de l'Union Les attaquants peuvent extraire des informations sur la version et la structure d'une base de données, ce qui leur permet d'exploiter d'autres informations. Cette technique peut varier d'une base de données à l'autre.
SQLi aveugle Avec Blind SQLi, les attaquants peuvent mettre en œuvre la requête sur une base de données. Le problème est que les attaquants contrôlent cette requête et qu'elle ne renvoie aucun résultat dans la réponse de l'application.
Subvertir la logique de l'application Les attaquants interfèrent avec une requête ou la manipulent pour perturber la logique de l'application. Pour manipuler la requête, les attaquants peuvent combiner la séquence de commentaires SQL "--" et une clause WHERE.

Types SQLi

Voyons maintenant les trois différents types de SQLi. 

SQLi en bande

Il s'agit de l'un des types d'injection SQL les plus courants, les plus simples et les plus efficaces. Dans ce type d'attaque, le même canal de communication est utilisé pour attaquer et récupérer le ou les résultats.

Les deux types d'attaques SQLi en bande sont décrits ci-dessous :

  • SQLi basé sur l'union - L'attaque basée sur l'union utilise l'opérateur d'union pour combiner deux ou plusieurs requêtes SQL, telles que des instructions SELECT, afin d'obtenir les informations souhaitées et les résultats dans une réponse HTTP GET.
  • ‍Error-basedSQLi - L'attaquant utilise les messages d'erreur de la base de données pour comprendre sa structure. Dans cette attaque, l'attaquant peut envoyer de fausses requêtes ou effectuer des actions pour que le serveur affiche des messages d'erreur afin qu'il puisse recevoir des informations sur la base de données. C'est pourquoi il est important que les développeurs évitent d'envoyer des messages d'erreur ou de journal dans l'environnement réel ; au lieu de cela, ils devraient être stockés avec un accès restreint.

SQLi inférentiel

Les attaques SQLi inférentielles ou aveugles sont plus compliquées et peuvent prendre plus de temps à exploiter. En outre, l'attaquant n'obtient pas immédiatement les résultats de l'attaque, ce qui en fait une attaque aveugle. 

L'attaquant envoie les charges utiles via des requêtes HTTP au serveur de base de données pour restructurer la base de données de l'utilisateur, puis il observe la réponse et le comportement de l'application pour voir si l'attaque a réussi ou non. 

Il s'agit de deux types d'attaques SQLi inférentielles :

  • SQLi aveugle basé sur les booléens - Dans cette attaque, une requête est envoyée à la base de données pour obtenir un résultat booléen (vrai ou faux), et l'attaquant observe la réponse HTTP pour prédire le résultat booléen.
  • SQLi aveugle basé sur le temps - Dans cette attaque, l'attaquant envoie une requête à la base de données pour la faire attendre quelques secondes avant d'envoyer la réponse, et l'attaquant juge les résultats de la requête à partir du temps de réponse de la demande HTTP.

SQLi hors bande

Il s'agit d'un type d'attaque SQLi plus rare qui dépend des fonctions activées du serveur de base de données. Elle se produit dans les cas où l'attaquant ne peut pas vraiment utiliser les autres types d'attaques.

Par exemple, s'il ne peut pas utiliser le même canal de communication pour l'attaque en bande, ou si la réponse HTTP n'est pas assez claire pour qu'il puisse déterminer les résultats de la requête.
En outre, ce type d'attaque n'est pas très courant, car il repose en grande partie sur la capacité du serveur de base de données à effectuer des requêtes HTTP ou DNS afin d'envoyer les données requises à l'auteur de l'attaque.

Comment se défendre contre SQLi

Heureusement, l'injection SQL est si ancienne et si courante qu'il existe des moyens de l'empêcher. L'utilisation de ce type de techniques de prévention n'est pas seulement une bonne habitude de codage, elle renforcera réellement la sécurité d'une organisation contre les injections SQL. 

Il existe de nombreux moyens de protéger les serveurs de base de données contre ce type d'attaques, comme la validation des entrées, l'utilisation d'un pare-feu d'application web (WAF), la sécurisation des bases de données, l'emploi d'équipes ou de systèmes de sécurité tiers et l'écriture de requêtes SQL infaillibles.

Examinons un exemple de prévention des injections SQL en Python en utilisant l'une des mesures de sécurité mentionnées ci-dessus.

Exemple Python

Dans cet exemple, l'attaquant utilisera une injection SQL aveugle basée sur des booléens pour s'emparer d'informations importantes du système. 

Python : Vulnérable

Supposons que la base de données contienne une table appelée "sample_data". Cette table stocke les noms d'utilisateur et les mots de passe des utilisateurs de l'application. 

Permettez maintenant à l'utilisateur de trouver une valeur dans cette table de base de données à l'aide des commandes suivantes :

import mysql.connector
db = mysql.connector.connect
#Mauvaise pratique. Evitez ceci ! C'est juste pour apprendre.
(host="localhost", user="newuser", passwd="pass", db="sample")
cur = db.cursor()
name = raw_input('Enter Name : ')
cur.execute("SELECT * FROM sample_data WHERE Name = '%s' ;" % name) for row in cur.fetchall() : print(row)
db.close()

Injection SQL

Ici, si l'utilisateur saisit un nom dans la recherche, par exemple Alicia, il n'y aura pas de problème avec le résultat. 

Toutefois, si l'utilisateur saisit quelque chose comme Alicia' ; DROP TABLE sample_data ; cela affectera la base de données de manière significative.

Python : Remédiation

L'instruction SQL doit être remplacée par l'instruction suivante pour empêcher l'attaque :

cur.execute("SELECT * FROM sample_data WHERE Name = %s ;", (name,))

Désormais, le système traitera l'entrée de l'utilisateur comme une chaîne, même si l'utilisateur tente d'y injecter des requêtes SQL, et traitera l'entrée de l'utilisateur comme la valeur du nom uniquement. 

Cette simple modification peut empêcher toute activité malveillante dans les futures requêtes et sécuriser le système contre les attaques par saisie de l'utilisateur.

Exemple Java

Pour cet exemple, nous utiliserons également une table de base de données nommée "sample_data" qui stocke les données de l'utilisateur de l'application. 

Une page de connexion de base prend un nom d'utilisateur et un mot de passe et le fichier Java, qui est une servlet (LoginServlet), les valide par rapport à la base de données pour permettre l'opération de connexion. 

Java : Exemple de vulnérabilité 

En utilisant la table "sample_data" de la base de données, le système permet aux utilisateurs d'effectuer des opérations de connexion en prenant leurs informations d'identification comme données d'entrée.

Le fichier LoginServlet contient une requête qui permet d'effectuer l'opération de connexion :

//Bad Example. Do not use string concatenation.
String query = "select * from sample_data where username='" + username + "' and password = '" + password + "'";
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                // Login Successful if match is found
                success = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                stmt.close();
                conn.close();
            } catch (Exception e) {}
        }
        if (success) {
            response.sendRedirect("home.html");
        } else {
            response.sendRedirect("login.html?error=1");
        }
    }

Voici la requête pour la connexion de l'utilisateur :

select * from sample_data where username='username' and password ='password'

Injection SQL

Le système fonctionnera parfaitement si l'entrée est valide. Par exemple, nous dirons que le nom d'utilisateur est à nouveau Alicia et que le mot de passe est secret. 

Le système renverra les données de l'utilisateur avec ces informations d'identification. Cependant, un attaquant peut manipuler la requête de l'utilisateur en utilisant Postman et cURL pour une injection SQL. 

Par exemple, le pirate peut envoyer un nom d'utilisateur fictif ( Alicia) et le mot de passe 'ou '1'='1'. 

Dans ce cas, le nom d'utilisateur et le mot de passe ne correspondent pas, mais la condition '1'='1' sera toujours vraie et l'opération de connexion sera réussie.

Java : Prévention

Pour éviter cela, nous devons modifier le code de LoginValidation et utiliser PreparedStatement au lieu de Statement pour l'exécution de la requête. Cette modification empêchera la concaténation du nom d'utilisateur et du mot de passe dans la requête et les traitera comme des données de type "setter" pour éviter l'injection SQL. 

Vous trouverez ci-dessous le code modifié pour LoginValidation :

String query = "select * from sample_data where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
    stmt = conn.prepareStatement(query);
    stmt.setString(1, username);
    stmt.setString(2, password);
    ResultSet rs = stmt.executeQuery();
    if (rs.next()) {
       success = true;
       }
      rs.close();
      } catch (Exception e) {
         e.printStackTrace();
         } finally {
             try {
                 stmt.close();
                 conn.close();
            } catch (Exception e) {
            }
         }

Dans ce cas, le PreparedStatement, les setters et l'API JDBC sous-jacente se chargeront de la saisie de l'utilisateur et empêcheront l'injection SQL.

Exemples

Nous allons maintenant examiner quelques autres exemples dans différentes langues afin de mieux comprendre ce que cela donne en pratique.

C# - Insecure

Cet exemple n'est pas sûr en raison de l'utilisation de la méthode `FromRawSql`. Cette méthode ne lie pas les paramètres et ne tente pas de les échapper. En tant que telle, cette méthode devrait être évitée à tout prix.

var blogs = context.Posts
    .FromRawSql("SELECT * FROM Posts WHERE state = {0} AND author = {1}", state, author)
    .ToList();

C# - Sécurisé

Cet exemple est sécurisé grâce à la fonction `FromSqlInterpolated`, qui prend les valeurs interpolées et les paramètre.

Bien que cette méthode soit généralement sûre, elle risque d'être très similaire à `FromRawSql`, qui n'est pas sûr. 

var blogs = context.Posts
    .FromSqlInterpolated($"SELECT * FROM Posts WHERE state = {state} AND author = {author}")
.ToList();

Java - Sécurisé : Hibernate - Named Query + Native Query

Hibernate offre deux méthodes pour construire des requêtes de manière sûre à travers ses `Native Query`, et `Named Query`. Ces deux méthodes permettent de spécifier des emplacements pour les paramètres.

@NamedNativeQuery(
        name = "find_post_by_state_and_author",
        query =
        "SELECT * " +
                "FROM Post " +
                "WHERE state = :state" + 
         " AND author = :author",
        resultClass = Post.class)

java
List<Post> posts = session.createNativeQuery(
        "SELECT * " +
        "FROM Post " +
        "WHERE state = :state" +
        " AND author = :author" )
        .addEntity(Post.class)
        .setParameter("state", state)
        .setParameter("author", author)
        .list();

Java - Sécurisé : jplq

En annotant un attribut `Query` sur l'interface d'un référentiel jplq, ils peuvent prendre plusieurs formes et être paramétrés.

@Query("SELECT p FROM Post p WHERE u.state = ?1 and u.author = ?2")
Post findPostByStateAndAuthor(String state, int author) ;
@Query("SELECT p FROM Post p WHERE u.state = :state and u.author = :author")
User findPostByStateAndAuthor(@Param("state") String state, @Param("author") int author) ;

Javascript - Secure : pg

Lorsque vous utilisez la bibliothèque `pg`, la méthode `query` permet le paramétrage en fournissant des valeurs de paramètres à travers son second paramètre.

const { posts } = await db.query('SELECT * FROM Post WHERE state = $1 AND author = $2', [state, author])

Javascript - Secure : Sequelize

La bibliothèque `sequelize` fournit un moyen de paramétrer une requête à travers son second argument, qui prend les paramètres de la requête. Cela inclut une liste de valeurs à lier à la requête en tant que paramètre, soit par nom, soit par index.

await sequelize.query(
    'SELECT * FROM Post WHERE state = $state AND author = $author',
    {
        bind: { state: state, author: author},
        type: QueryTypes.SELECT
    }
);
await sequelize.query(
    'SELECT * FROM Post WHERE state = $1 AND author = $2',
    {
        bind: [state, author],
        type: QueryTypes.SELECT
    }
);