Към съдържанието
wannacode

SQL injection и параметрите

Накратко: SQL injection е атака, при която текст от потребителя, долепен в заявката, се изпълнява като SQL, а заявката с параметри я спира.

Проблем

Търсачката на админ панела

В админ панела има поле „Имейл на клиента“. Програмата взима написаното и го слага в заявката:

SELECT name, email, city FROM customers WHERE email = '<написаното>'

С maria.ivanova@example.com излиза Мария. Но написаното може да е всякакъв текст, включително кавичка. Тогава то спира да е стойност и става част от заявката. Това се казва SQL injection: посетителят пише SQL на мястото на програмата.

Пробвай

Покажи всички клиенти

Пробвай сам. Целта е страницата да покаже всички клиенти, без да знаеш нито един имейл. Гледай реда „Какво стига до базата“: точно този текст пуска базата.

Страницата търси клиент по имейл.база: магазин

Какво стига до базата

По-точно, за любопитните

Подсказка: кавичка затваря текста, а после ти трябва условие, което е вярно за всеки ред. Например x' OR '1'='1. Последната кавичка не я пишеш, защото програмата я добавя сама.

Виж го

Още два трика

Входът не е задължително да е хитро условие. Коментарът -- маха всичко след себе си, включително проверка, която програмата е сложила след имейла:

SQLбаза: магазин

Програмата търси клиент с този имейл от Варна, а излиза Мария от София: проверката за града остана в коментара.

Ако драйверът пуска няколко заявки от един текст, входът x'; DELETE FROM customers; -- добавя цяла нова заявка. Пробвай го в полето горе (после „Нова база“). В истинския свят такъв вход трие таблици, сменя пароли или чете данни, които не са за него.

Идеята

Същото в кода: четири драйвера, четири поведения

Ето търсенето с долепен текст на четирите езика. Програмата търси три неща: истински имейл, x' OR '1'='1 и x'; DELETE FROM customers; --, и накрая брои клиентите:

static void find(Connection conn, String email) throws SQLException {
String sql = "SELECT name FROM customers WHERE email = '" + email + "'";
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql)) {
List<String> names = new ArrayList<>();
while (rs.next()) {
names.add(rs.getString("name"));
}
System.out.println(email + " → " + names);
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("грешка: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("клиенти в базата: " + rs.getInt(1));
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("грешка: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("клиенти в базата: " + rs.getInt(1));
}
}
Изход
maria.ivanova@example.com → [Мария Иванова]
x' OR '1'='1 → [Мария Иванова, Георги Петров, Ивана Колева, Димитър Георгиев, Радост Христова]
x'; DELETE FROM customers; -- → []
клиенти в базата: 5

С OR '1'='1' и четирите показват всички клиенти. Втората заявка в един текст е различна история. В C# Microsoft.Data.Sqlite я изпълнява и клиентите стават 0. В Java executeQuery и в JavaScript prepare пускат само първата заявка и мълчат за останалото, а Python спира с грешка. Драйверът не те пази: той само решава колко голяма да е щетата.

Идеята

Параметърът

Заявката има ? (в C# $email) там, където идва стойността, а стойността отива отделно. Базата първо разбира заявката, после слага стойността, и тя остава текст, каквото и да има в нея:

static void find(Connection conn, String email) throws SQLException {
String sql = "SELECT name FROM customers WHERE email = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, email);
try (ResultSet rs = ps.executeQuery()) {
List<String> names = new ArrayList<>();
while (rs.next()) {
names.add(rs.getString("name"));
}
System.out.println(email + " → " + names);
}
}
}
try (Connection conn = DriverManager.getConnection("jdbc:sqlite::memory:")) {
setup(conn);
find(conn, "maria.ivanova@example.com");
find(conn, "x' OR '1'='1");
try {
find(conn, "x'; DELETE FROM customers; --");
} catch (SQLException e) {
System.out.println("грешка: " + e.getMessage());
}
try (Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM customers")) {
rs.next();
System.out.println("клиенти в базата: " + rs.getInt(1));
}
}
Изход
maria.ivanova@example.com → [Мария Иванова]
x' OR '1'='1 → []
x'; DELETE FROM customers; -- → []
клиенти в базата: 5

Сега x' OR '1'='1 е просто имейл, какъвто няма, и резултатът е празен. В полето горе избери „Като параметър“ и пусни същите входове: заявката, която стига до базата, вече не се променя.

По-точно, за любопитните

Параметри има на всеки език и във всеки драйвер: PreparedStatement в Java, Parameters в C#, вторият аргумент на execute в Python, аргументите на all / get / run в node:sqlite. PostgreSQL драйверите ползват $1, $2 (pg) или %s (psycopg). Обикновено параметърът ускорява и повторните заявки, защото базата може да ползва вече разбраната заявка.

Параметърът е за стойности. Имената на таблици и колони (например колоната в ORDER BY от падащо меню) не могат да са параметри: там програмата сравнява входа със списък на позволените (price, name) и не пуска нищо друго.

Упражнение

Търсене по част от имейла

Търсачката приема и част от имейла. Програмата праща заявката от полето с параметър „petrov“. Смени ? със стойността, за да видиш какво ще върне базата.

SQLбаза: магазин

Тест

Провери се

1. Програмата прави заявката така: "... WHERE email = '" + email + "'". Кое я прави безопасна?
2. Защо „x' OR '1'='1“ връща всички клиенти?
3. Може ли параметър да замени името на колоната в ORDER BY (ORDER BY ?)?
4. В Python cursor.execute() с две заявки в текста дава грешка. Значи ли това, че injection не е опасна в Python?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·