Aperçu
Dans un système de gestion de base de données relationnelle (SGBDR), il existe un langage spécifique, appelé SQL (langage de requête structurée), qui est utilisé pour communiquer avec la base de données. Les instructions de requête écrites en SQL sont utilisées pour manipuler le contenu et la structure de la base de données. Une instruction SQL spécifique qui crée et modifie la structure de la base de données est appelée une instruction DDL (Data Definition Language) et les instructions qui manipulent le contenu de la base de données sont appelées une instruction DML (Data Manipulation Language). Le moteur associé au package RDBMS analyse et interprète l'instruction SQL et renvoie le résultat en conséquence. C'est le processus typique de communication avec RDBMS - lancez une instruction SQL et récupérez le résultat, c'est tout. Le système ne juge pas l'intention d'une déclaration qui adhère à la syntaxe et à la structure sémantique du langage. Cela signifie également qu'il n'y a pas de processus d'authentification ou de validation pour vérifier qui a déclenché l'instruction et le privilège dont on dispose pour obtenir la sortie. Un attaquant peut simplement déclencher une instruction SQL avec une intention malveillante et récupérer des informations qu'il n'est pas censé obtenir. Par exemple, un attaquant peut exécuter une instruction SQL avec une charge utile malveillante avec la requête apparemment inoffensive pour contrôler le serveur de base de données d'une application Web.
Comment ça marche
Un attaquant peut exploiter cette vulnérabilité et l'utiliser à son propre avantage. Par exemple, on peut contourner le mécanisme d'authentification et d'autorisation d'une application et récupérer des contenus dits sécurisés dans l'ensemble de la base de données. Une injection SQL peut être utilisée pour créer, mettre à jour et supprimer des enregistrements de la base de données. On peut donc formuler une requête limitée à sa propre imagination avec SQL.
En règle générale, une application lance fréquemment des requêtes SQL vers la base de données à de nombreuses fins, que ce soit pour récupérer certains enregistrements, créer des rapports, authentifier l'utilisateur, les transactions CRUD, etc. L'attaquant doit simplement trouver une requête d'entrée SQL dans un formulaire d'entrée d'application. La requête préparée par le formulaire peut ensuite être utilisée pour lier le contenu malveillant afin que, lorsque l'application déclenche la requête, elle transporte également la charge utile injectée.
L'une des situations idéales est lorsqu'une application demande à l'utilisateur une entrée telle qu'un nom d'utilisateur ou un identifiant d'utilisateur. L'application a ouvert un point vulnérable là-bas. L'instruction SQL peut être exécutée sans le savoir. Un attaquant en profite en injectant une charge utile qui sera utilisée dans le cadre de la requête SQL et traitée par la base de données. Par exemple, le pseudo-code côté serveur d'une opération POST pour un formulaire de connexion peut être :
uname = getRequestString("username"); pass = getRequestString("passwd"); stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "'"; database.execute(stmtSQL);
Le code précédent est vulnérable aux attaques par injection SQL car l'entrée donnée à l'instruction SQL via la variable "uname" et "pass" peut être manipulée d'une manière qui modifierait la sémantique de l'instruction.
Par exemple, nous pouvons modifier la requête pour qu'elle s'exécute sur le serveur de base de données, comme dans MySQL.
stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";
Cela entraîne la modification de l'instruction SQL d'origine à un degré qui permet de contourner l'authentification. Il s'agit d'une vulnérabilité grave et doit être empêchée dans le code.
Défense contre une attaque par injection SQL
L'un des moyens de réduire le risque d'attaque par injection SQL consiste à s'assurer que les chaînes de texte non filtrées ne doivent pas être autorisées à être ajoutées à l'instruction SQL avant l'exécution. Par exemple, nous pouvons utiliser PreparedStatement pour effectuer les tâches de base de données requises. L'aspect intéressant de PreparedStatement est qu'il envoie une instruction SQL pré-compilée à la base de données, plutôt qu'une chaîne. Cela signifie que la requête et les données sont envoyées séparément à la base de données. Cela empêche la cause première de l'attaque par injection SQL, car dans l'injection SQL, l'idée est de mélanger du code et des données dans lesquelles les données font en fait partie du code sous l'apparence de données. Dans DéclarationPréparée , il y a plusieurs setXYZ() méthodes, telles que setString() . Ces méthodes sont utilisées pour filtrer les caractères spéciaux tels qu'une citation contenue dans les instructions SQL.
Par exemple, nous pouvons exécuter une instruction SQL de la manière suivante.
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;
Au lieu de mettre, disons, eno=10125 en tant que numéro d'employé dans l'entrée, nous pouvons modifier la requête avec l'entrée telle que :
eno = 10125 OR 1=1
Cela change complètement le résultat renvoyé par la requête.
Un exemple
Dans l'exemple de code suivant, nous avons montré comment PreparedStatement peut être utilisé pour effectuer des tâches de base de données.
package org.mano.example; import java.sql.*; import java.time.LocalDate; public class App { static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost:3306/employees"; static final String USER = "root"; static final String PASS = "secret"; public static void main( String[] args ) { String selectQuery = "SELECT * FROM employees WHERE emp_no = ?"; String insertQuery = "INSERT INTO employees VALUES (?,?,?,?,?,?)"; String deleteQuery = "DELETE FROM employees WHERE emp_no = ?"; Connection connection = null; try { Class.forName(JDBC_DRIVER); connection = DriverManager.getConnection (DB_URL, USER, PASS); }catch(Exception ex) { ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(insertQuery);){ pstmt.setInt(1,99); pstmt.setDate(2, Date.valueOf (LocalDate.of(1975,12,11))); pstmt.setString(3,"ABC"); pstmt.setString(4,"XYZ"); pstmt.setString(5,"M"); pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1))); pstmt.executeUpdate(); System.out.println("Record inserted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(selectQuery);){ pstmt.setInt(1,99); ResultSet rs = pstmt.executeQuery(); while(rs.next()){ System.out.println(rs.getString(3)+ " "+rs.getString(4)); } }catch(Exception ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(deleteQuery);){ pstmt.setInt(1,99); pstmt.executeUpdate(); System.out.println("Record deleted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try{ connection.close(); }catch(Exception ex){ ex.printStackTrace(); } } }
Un aperçu de PreparedStatement
Ces travaux peuvent également être accomplis avec une instruction JDBC interface, mais le problème est qu'elle peut parfois être assez peu sûre, en particulier lorsqu'une instruction SQL dynamique est exécutée pour interroger la base de données où les valeurs d'entrée de l'utilisateur sont concaténées avec les requêtes SQL. Cela peut être une situation dangereuse, comme nous l'avons vu. Dans la plupart des circonstances ordinaires, Déclaration est tout à fait inoffensif, mais PreparedStatement semble être la meilleure option entre les deux. Il empêche la concaténation des chaînes malveillantes en raison de son approche différente lors de l'envoi de l'instruction à la base de données. DéclarationPréparée utilise la substitution de variables plutôt que la concaténation. Placer un point d'interrogation (?) dans la requête SQL signifie qu'une variable de substitution prendra sa place et fournira la valeur lors de l'exécution de la requête. La position de la variable de substitution prend sa place en fonction de la position de l'index du paramètre attribué dans le setXYZ() méthodes.
Cette technique l'empêche d'attaquer par injection SQL.
En outre, PreparedStatement implémente AutoCloseable. Cela lui permet d'écrire dans le contexte d'un try-with-resources bloquer et se ferme automatiquement lorsqu'il sort de la portée.
Conclusion
Une attaque par injection SQL ne peut être évitée qu'en écrivant le code de manière responsable. En fait, dans toute solution logicielle, la sécurité est principalement compromise en raison de mauvaises pratiques de codage. Ici, nous avons décrit ce qu'il faut éviter et comment PreparedStatement peut nous aider à écrire un code sécurisé. Pour une idée complète sur l'injection SQL, reportez-vous aux documents appropriés ; Internet en regorge, et, pour PreparedStatement , consultez la documentation de l'API Java pour une explication plus détaillée.