Общ преглед
В системата за управление на релационни бази данни (RDBMS) има специфичен език – наречен SQL (език на структурирани запитвания), който се използва за комуникация с базата данни. Изразите за заявка, написани на SQL, се използват за манипулиране на съдържанието и структурата на базата данни. Конкретен SQL оператор, който създава и променя структурата на базата данни, се нарича DDL (език за дефиниране на данни), а операторите, които манипулират съдържанието на базата данни, се наричат DML (език за манипулиране на данни). Машината, свързана с пакета RDBMS, анализира и интерпретира SQL израза и съответно връща резултата. Това е типичният процес на комуникация с RDBMS – задействайте SQL оператор и върнете резултата, това е всичко. Системата не преценява намерението на всяко твърдение, което се придържа към синтаксиса и семантичната структура на езика. Това също означава, че няма процеси за удостоверяване или валидиране, за да се провери кой е задействал изявлението и привилегията, която има при получаване на изхода. Нападателят може просто да задейства SQL изявление със злонамерено намерение и да получи обратно информация, която не би трябвало да получи. Например, нападателят може да изпълни SQL израз със злонамерен полезен товар с безобидно изглеждаща заявка за контрол на сървъра на база данни на уеб приложение.
Как работи
Нападателят може да използва тази уязвимост и да я използва в своя полза. Например, човек може да заобиколи механизма за удостоверяване и оторизация на приложението и да извлече така нареченото защитено съдържание от цялата база данни. SQL инжекция може да се използва за създаване, актуализиране и изтриване на записи от базата данни. Следователно човек може да формулира заявка, ограничена до собственото си въображение с SQL.
Обикновено едно приложение често задейства SQL заявки към базата данни за множество цели, било то за извличане на определени записи, създаване на отчети, удостоверяване на потребител, CRUD транзакции и т.н. Нападателят просто трябва да намери SQL заявка за въвеждане в някаква форма за въвеждане на приложение. След това заявката, подготвена от формуляра, може да се използва за преплитане на злонамерено съдържание, така че, когато приложението задейства заявката, то носи и инжектирания полезен товар.
Една от идеалните ситуации е, когато приложение поиска от потребителя въвеждане като потребителско име или потребителски идентификатор. Приложението отвори уязвимо място там. SQL операторът може да се изпълнява несъзнателно. Нападателят се възползва, като инжектира полезен товар, който да се използва като част от SQL заявката и да се обработва от базата данни. Например псевдокодът от страна на сървъра за POST операция за формуляр за вход може да бъде:
uname = getRequestString("username"); pass = getRequestString("passwd"); stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "'"; database.execute(stmtSQL);
Предходният код е уязвим за атака с инжектиране на SQL, тъй като входът, даден на SQL израза чрез променливите „uname“ и „pass“, може да бъде манипулиран по начин, който би променил семантиката на оператора.
Например, можем да модифицираме заявката да се изпълнява срещу сървъра на базата данни, както в MySQL.
stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";
Това води до промяна на оригиналния SQL израз до степен, която позволява да се заобиколи удостоверяването. Това е сериозна уязвимост и трябва да бъде предотвратена от кода.
Защита срещу атака с инжектиране на SQL
Един от начините за намаляване на вероятността от атака с инжектиране на SQL е да се гарантира, че нефилтрираните низове от текст не трябва да бъдат допускани да се добавят към SQL израза преди изпълнение. Например, можем да използваме PreparedStatement за изпълнение на необходимите задачи на базата данни. Интересният аспект на PreparedStatement е, че изпраща предварително компилиран SQL израз към базата данни, а не низ. Това означава, че заявката и данните се изпращат отделно към базата данни. Това предотвратява основната причина за атаката с инжектиране на SQL, тъй като при SQL инжекцията идеята е да се смесват код и данни, при което данните всъщност са част от кода под прикритието на данни. В PreparedStatement , има множество setXYZ() методи, като setString() . Тези методи се използват за филтриране на специални символи, като цитат, съдържащ се в SQL изразите.
Например, можем да изпълним SQL оператор по следния начин.
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;
Вместо да поставите, да речем, eno=10125 като номер на служител във входа, можем да модифицираме заявката с входа, като например:
eno = 10125 OR 1=1
Това напълно променя резултата, върнат от заявката.
Пример
В следващия примерен код ние показахме как PreparedStatement може да се използва за изпълнение на задачи с база данни.
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(); } } }
Поглед в PreparedStatement
Тези задачи също могат да бъдат извършени с JDBC Изявление интерфейс, но проблемът е, че понякога може да бъде доста несигурен, особено когато се изпълнява динамичен SQL израз за запитване на базата данни, където стойностите, въведени от потребителя, са свързани със SQL заявките. Това може да бъде опасна ситуация, както видяхме. При най-обикновени обстоятелства Изявление е доста безвреден, но PreparedStatement изглежда е по-добрият вариант между двете. Той предотвратява свързването на злонамерени низове поради различния си подход при изпращане на изявлението към базата данни. PreparedStatement използва заместване на променлива, а не конкатенация. Поставянето на въпросителен знак (?) в SQL заявката означава, че заместваща променлива ще заеме нейното място и ще предостави стойността, когато заявката бъде изпълнена. Позицията на заместващата променлива заема своето място според присвоената позиция на индекса на параметъра в setXYZ() методи.
Тази техника го предотвратява от атака с инжектиране на SQL.
Освен това PreparedStatement прилага AutoCloseable. Това му позволява да пише в контекста на опитайте с ресурси блокира и автоматично се затваря, когато излезе извън обхвата.
Заключение
Атаката с инжектиране на SQL може да бъде предотвратена само чрез отговорно писане на кода. Всъщност във всяко софтуерно решение сигурността е най-вече нарушена поради лоши практики на кодиране. Тук сме описали какво да избягвате и как PreparedStatement може да ни помогне да напишем защитен код. За пълна представа относно SQL инжектирането вижте подходящи материали; Интернет е пълен с тях, а за PreparedStatement , разгледайте документацията на Java API за по-подробно обяснение.