SQL injection и параметрите
Накратко: SQL injection е атака, при която текст от потребителя, долепен в заявката, се изпълнява като SQL, а заявката с параметри я спира.
Проблем
Търсачката на админ панела
В админ панела има поле „Имейл на клиента“. Програмата взима написаното и го слага в заявката:
SELECT name, email, city FROM customers WHERE email = '<написаното>'С maria.ivanova@example.com излиза Мария. Но написаното може да е всякакъв текст, включително кавичка. Тогава то спира да е стойност и става част от заявката. Това се казва SQL injection: посетителят пише SQL на мястото на програмата.
Пробвай
Покажи всички клиенти
Пробвай сам. Целта е страницата да покаже всички клиенти, без да знаеш нито един имейл. Гледай реда „Какво стига до базата“: точно този текст пуска базата.
Какво стига до базата
По-точно, за любопитните
Подсказка: кавичка затваря текста, а после ти трябва условие, което е вярно за всеки ред. Например x' OR '1'='1. Последната кавичка не я пишеш, защото програмата я добавя сама.
Виж го
Още два трика
Входът не е задължително да е хитро условие. Коментарът -- маха всичко след себе си, включително проверка, която програмата е сложила след имейла:
Програмата търси клиент с този имейл от Варна, а излиза Мария от София: проверката за града остана в коментара.
Ако драйверът пуска няколко заявки от един текст, входът 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)); }}using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"грешка: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"клиенти в базата: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = $"SELECT name FROM customers WHERE email = '{email}'"; using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}def find(conn, email): sql = f"SELECT name FROM customers WHERE email = '{email}'" names = [name for (name,) in conn.execute(sql)] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("грешка:", e)print("клиенти в базата:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()function find(db, email) { const sql = `SELECT name FROM customers WHERE email = '${email}'`; const names = db.prepare(sql).all().map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("грешка:", e.message);}console.log("клиенти в базата:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();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; -- → []клиенти в базата: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"грешка: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"клиенти в базата: {count.ExecuteScalar()}");maria.ivanova@example.com → [Мария Иванова]x' OR '1'='1 → [Мария Иванова, Георги Петров, Ивана Колева, Димитър Георгиев, Радост Христова]x'; DELETE FROM customers; -- → []клиенти в базата: 0conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("грешка:", e)print("клиенти в базата:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Мария Иванова]x' OR '1'='1 → [Мария Иванова, Георги Петров, Ивана Колева, Димитър Георгиев, Радост Христова]грешка: You can only execute one statement at a time.клиенти в базата: 5const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("грешка:", e.message);}console.log("клиенти в базата:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();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; -- → []клиенти в базата: 5using var conn = new SqliteConnection("Data Source=:memory:");conn.Open();Setup(conn);Find(conn, "maria.ivanova@example.com");Find(conn, "x' OR '1'='1");try{ Find(conn, "x'; DELETE FROM customers; --");}catch (SqliteException e){ Console.WriteLine($"грешка: {e.Message}");}var count = conn.CreateCommand();count.CommandText = "SELECT COUNT(*) FROM customers";Console.WriteLine($"клиенти в базата: {count.ExecuteScalar()}");
static void Find(SqliteConnection conn, string email){ var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT name FROM customers WHERE email = $email"; cmd.Parameters.AddWithValue("$email", email); using var reader = cmd.ExecuteReader(); var names = new List<string>(); while (reader.Read()) { names.Add(reader.GetString(0)); } Console.WriteLine($"{email} → [{string.Join(", ", names)}]");}maria.ivanova@example.com → [Мария Иванова]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []клиенти в базата: 5def find(conn, email): sql = "SELECT name FROM customers WHERE email = ?" names = [name for (name,) in conn.execute(sql, (email,))] print(f"{email} → [{', '.join(names)}]")
conn = sqlite3.connect(":memory:")setup(conn)find(conn, "maria.ivanova@example.com")find(conn, "x' OR '1'='1")try: find(conn, "x'; DELETE FROM customers; --")except sqlite3.Error as e: print("грешка:", e)print("клиенти в базата:", conn.execute("SELECT COUNT(*) FROM customers").fetchone()[0])conn.close()maria.ivanova@example.com → [Мария Иванова]x' OR '1'='1 → []x'; DELETE FROM customers; -- → []клиенти в базата: 5function find(db, email) { const sql = "SELECT name FROM customers WHERE email = ?"; const names = db.prepare(sql).all(email).map((row) => row.name); console.log(`${email} → [${names.join(", ")}]`);}
const db = new DatabaseSync(":memory:");setup(db);find(db, "maria.ivanova@example.com");find(db, "x' OR '1'='1");try { find(db, "x'; DELETE FROM customers; --");} catch (e) { console.log("грешка:", e.message);}console.log("клиенти в базата:", db.prepare("SELECT COUNT(*) AS n FROM customers").get().n);db.close();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“. Смени ? със стойността, за да видиш какво ще върне базата.
Решение
SELECT name, email FROM customers WHERE email LIKE '%' || 'petrov' || '%';% отпред и отзад се слепват със стойността в самата заявка ('%' || ? || '%'), а не във входа. Така параметърът остава само текст, а LIKE получава %petrov%.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: IN, BETWEEN и LIKE · Текстови функции · DELETE: изтриване на редове · Връзка с базата от код
- Продължава в: ORM и проблемът N+1 · Пул от връзки, резервни копия и права
- Задачи по темата: Базата в програмата