Как всего один символ способен вскрыть чужую базу данных.

SQL-инъекция — одна из древнейших и одновременно опаснейших уязвимостей в веб-приложениях. Она позволяет злоумышленнику управлять запросами к базе данных с помощью поддельных данных, которые приложение ошибочно воспринимает как часть запроса. Последствия варьируются от утечки отдельных записей до полного захвата базы данных и дальнейших действий в инфраструктуре.
Когда приложение формирует SQL-запрос, подставляя в него данные от пользователя без надлежащей обработки, атакующий может вставить дополнительные операторы или конструкции. В результате сервер баз данных выполнит не то, что ожидал разработчик, а то, что заставил выполнить злоумышленник.
Простая аналогия: если вы даёте секретарю готовую фразу и не контролируете, что он вписывает между слов, кто-то может подменить начало или конец предложения так, что смысл поменяется кардинально.
Типичный сценарий выглядит так. Веб-форма принимает ввод от пользователя, например логин и пароль. Сервер получает эти строки и формирует SQL-запрос, подставляя значения прямо в текст запроса. Если разработчик не экранирует и не использует параметризацию, то строка ввода становится частью кода SQL.
/* уязвимый пример */
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT id FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($db, $query);
Если в поле username злоумышленник введёт строку типа ' OR '1'='1, итоговый SQL превратится в логическое выражение, которое всегда истинно, и авторизация будет пройдена.
┌─────────────────────────────────────────────────────────────────┐
│ ЛЕГИТИМНЫЙ ЗАПРОС │
├─────────────────────────────────────────────────────────────────┤
│ SELECT id FROM users │
│ WHERE username = 'john' AND password = 'secret123' │
│ │
│ Результат: Проверяет оба условия │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ АТАКА С ИНЪЕКЦИЕЙ │
├─────────────────────────────────────────────────────────────────┤
│ SELECT id FROM users │
│ WHERE username = 'admin' OR '1'='1' AND password = '' │
│ ^^^^^^^^^^^ │
│ всегда TRUE! │
│ │
│ Результат: Обход авторизации — вход без пароля! │
└─────────────────────────────────────────────────────────────────┘
С практической точки зрения важны несколько разновидностей инъекций. Каждая имеет свои признаки и технику эксплуатации.
In-band означает, что злоумышленник получает данные тем же каналом, через который посылает запросы. Два популярных подтипа — error-based и union-based.
Error-based использует сообщения об ошибках базы данных, чтобы «вытащить» фрагменты данных. Union-based подставляет UNION SELECT для соединения результатов атакующего с результатом легитимного запроса.
/* пример union-based */
?id=10 UNION SELECT null, username, password FROM users --
┌────────────────────────────────────┐ ┌────────────────────────────────────┐
│ ОРИГИНАЛЬНЫЙ ЗАПРОС │ │ ВРЕДОНОСНЫЙ ЗАПРОС │
├────────────────────────────────────┤ ├────────────────────────────────────┤
│ SELECT name, price, desc │ UNION│ SELECT username, password, email │
│ FROM products │ │ FROM users -- │
│ WHERE id=10 │ │ │
└────────────────────────────────────┘ └────────────────────────────────────┘
│ │
└──────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ ОБЪЕДИНЕННЫЙ РЕЗУЛЬТАТ │
├─────────────────────────────────────────────┤
│ Laptop | $999 | Gaming │
│ admin | $2y$10$abc... | admin@example.com │ ← УТЕЧКА!
│ john | $2y$10$xyz... | john@example.com │ ← УТЕЧКА!
└─────────────────────────────────────────────┘
Атакующий видит данные пользователей прямо на странице товара!
Когда приложение не возвращает содержательное сообщение об ошибке или не выводит данные напрямую, атакующий использует слепую инъекцию. Он посылает логические запросы и наблюдает поведение приложения: отвечает ли страница иначе, зависит ли время отклика от условия и т. п.
Есть два популярных подхода: boolean-based и time-based. В первом атакующий ставит условие и смотрит, меняется ли содержимое страницы. Во втором — вызывает задержку в базе (WAITFOR DELAY, pg_sleep()) чтобы измерить время ответа.
/* пример boolean-based */
... AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1) = 'a'
/* пример time-based */
... AND (SELECT CASE WHEN (SUBSTR(password,1,1) = 'a') THEN pg_sleep(5) ELSE pg_sleep(0) END)
АТАКУЮЩИЙ СЕРВЕР
│ │
│ 1. Проверка первого символа │
├──────────────────────────────►
│ ?id=1 AND │
│ SUBSTR(password,1,1)='a' │
│ │
│ ◄────────────────────┤
│ 404 Not Found │
│ (условие ложно) │
│ │
│ 2. Пробуем следующий символ │
├──────────────────────────────►
│ ?id=1 AND │
│ SUBSTR(password,1,1)='b' │
│ │
│ ◄────────────────────┤
│ 200 OK (страница!) │
│ (условие истинно!) │
│ │
│ ✓ Первый символ = 'b' │
│ │
│ 3. Повторяем для 2-го, 3-го │
│ символа и т.д... │
│ │
▼ ▼
Результат: Атакующий посимвольно извлекает пароль,
анализируя изменения в ответах сервера (200 vs 404)
Иногда злоумышленник заставляет базу данных связаться с внешним сервером атакующего, чтобы передать данные. Это используют при ограниченном канале вывода. Примеры: вызов функции для разрешения DNS-запроса с подставленным содержимым или отправка HTTP-запроса.
Такой сценарий особенно коварен, потому что данные извлекаются напрямую из БД и уходят на внешний ресурс, минуя логический вывод сайта.
SQL-инъекция второго порядка (Second-Order SQL Injection) — это атака, при которой вредоносный код внедряется в базу данных безопасным способом (например, через параметризованный запрос), но затем извлекается и используется в другом, уязвимом месте приложения без должной обработки.
admin' --. Благодаря параметризованным запросам, это значение безопасно сохраняется в базе данных как обычная строка.// ЭТАП 1: Регистрация пользователя (БЕЗОПАСНО)
$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->execute(["admin' --", "[email protected]"]);
// Значение "admin' --" безопасно сохранено в БД
// ЭТАП 2: Админ-панель показывает активность (УЯЗВИМО!)
$username = $row['username']; // Считали из БД: "admin' --"
// ОШИБКА: формируем запрос конкатенацией!
$query = "SELECT * FROM activity WHERE user = '$username'";
$result = mysqli_query($db, $query);
// Итоговый SQL:
// SELECT * FROM activity WHERE user = 'admin' --'
// Все после -- игнорируется — атака успешна!
═══════════════════════════════════════════════════════════════════════
ЭТАП 1: БЕЗОПАСНОЕ СОХРАНЕНИЕ
═══════════════════════════════════════════════════════════════════════
Регистрация нового пользователя:
┌─────────────────────────────────┐
│ Username: admin' -- │
│ Email: [email protected] │
└─────────────────────────────────┘
│
│ Код использует параметризацию:
│ $stmt->execute(["admin' --", ...]) ✓ Безопасно!
▼
┌─────────────────────────────────┐
│ БАЗА ДАННЫХ │
├─────────────────────────────────┤
│ username: "admin' --" │
│ (сохранено как обычный текст) │
└─────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════
ЭТАП 2: УЯЗВИМОЕ ИСПОЛЬЗОВАНИЕ
═══════════════════════════════════════════════════════════════════════
Админ-панель считывает данные из БД:
$username = $row['username']; // Значение: "admin' --"
⚠️ ОШИБКА: Конкатенация строк вместо параметризации!
┌────────────────────────────────────────────────────────────────┐
│ $query = "SELECT * FROM activity WHERE user = '$username'"; │
└────────────────────────────────────────────────────────────────┘
Итоговый SQL, отправленный в БД:
┌────────────────────────────────────────────────────────────────┐
│ SELECT * FROM activity WHERE user = 'admin' --' │
│ ^^^^^^ │
│ Комментарий отсекает │
│ остальное! │
└────────────────────────────────────────────────────────────────┘
Результат: Атака срабатывает на ВТОРОМ этапе, когда "безопасные"
данные из БД используются в уязвимом запросе!
// ПРАВИЛЬНЫЙ подход для этапа 2:
$username = $row['username']; // Считали из БД
// Снова используем параметризацию!
$stmt = $pdo->prepare("SELECT * FROM activity WHERE user = ?");
$stmt->execute([$username]); // Безопасно!
Признаки могут быть очевидными и тонкими. Набор сигналов помогает оператору быстрее заметить инцидент.
Среди признаков стоит отметить:
--, /* */ или ключевые слова SQLВажно: один признак сам по себе не доказывает инъекцию, но набор совпадающих тревог сильно повышает вероятность. Для точного установления надо анализировать логи, дамп БД и сетевые соединения.
Ниже несколько реальных по форме, но упрощённых по содержанию примеров, которые показывают широкий спектр возможностей SQL-инъекций.
Исходный уязвимый код уже показан выше. Если поле пароля пустое, злоумышленник может ввести в поле username: admin' --. В итоговом SQL всё после комментария игнорируется и проверка пароля пропускается.
/* итоговый SQL */
SELECT id FROM users WHERE username = 'admin' --' AND password = ''
/* все после -- это комментарий, проверка пароля отключена */
Предположим уязвимый параметр ?product_id=, возвращающий 3 столбца. Атакующий подставляет UNION SELECT, чтобы присоединить результаты таблицы users.
?product_id=1 UNION SELECT id, username, password FROM users --
Если сайт выводит объединённые строки, attacker увидит логины и хэши паролей прямо в интерфейсе.
Атакующий последовательно проверяет каждый символ пароля, ставя условие и наблюдая изменения страницы. Метод медленный, но работает даже при минимальных ответах.
/* boolean-based */
?id=10 AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1) = 'a'
/* повторять с 'b','c' и т.д., затем смещаться на второй символ */
Если база поддерживает функции для сетевых запросов, злоумышленник может вызвать разрешение имени вида secrethash.attacker.com, где secrethash — часть данных. В DNS-сервере атакующего появится запрос с нужной информацией.
Последствия зависят от прав, с которыми работает процесс базы данных, и от архитектуры приложения. Возможные сценарии:
Даже если атакующему не удалось скачать всю базу, наличие следов компрометации серьёзно подрывает доверие и требует полного инцидент-ответа.
Защита строится по принципу многоуровневой безопасности. Одна только фильтрация входа не спасёт, если другие слои пробиты. Рассмотрим основные меры защиты от SQL-инъекций.
Это основной и самый надёжный способ защиты. Подготовленные выражения отделяют код от данных, и любые кавычки в вводе не меняют структуру SQL. База данных получает структуру запроса отдельно от данных, что исключает возможность интерпретации пользовательского ввода как SQL-кода.
PHP (PDO):
/* Безопасный пример */
$stmt = $pdo->prepare('SELECT id FROM users WHERE username = ? AND password = ?');
$stmt->execute([$username, $password]);
Python (psycopg2 для PostgreSQL):
import psycopg2
# Безопасный пример
cursor = conn.cursor()
cursor.execute(
"SELECT id FROM users WHERE username = %s AND password = %s",
(username, password)
)
result = cursor.fetchone()
Python (sqlite3):
import sqlite3
# Безопасный пример
cursor = conn.cursor()
cursor.execute(
"SELECT id FROM users WHERE username = ? AND password = ?",
(username, password)
)
result = cursor.fetchone()
Node.js (node-postgres / pg):
const { Pool } = require('pg');
const pool = new Pool();
// Безопасный пример
const result = await pool.query(
'SELECT id FROM users WHERE username = $1 AND password = $2',
[username, password]
);
Node.js (MySQL2):
const mysql = require('mysql2/promise');
// Безопасный пример
const [rows] = await connection.execute(
'SELECT id FROM users WHERE username = ? AND password = ?',
[username, password]
);
В большинстве современных приложений разработчики редко пишут SQL напрямую. Вместо этого используются ORM (Object-Relational Mapping) — библиотеки, которые позволяют работать с базой данных через объекты и методы, автоматически генерируя безопасный SQL. ORM автоматически применяют параметризацию "под капотом" и являются основным барьером против SQL-инъекций для большинства современных CRUD-операций.
Популярные ORM включают Eloquent (Laravel), Doctrine (Symfony), SQLAlchemy и Django ORM (Python), TypeORM, Sequelize и Prisma (Node.js). Все они обеспечивают безопасность по умолчанию, если разработчик использует их встроенные методы и избегает "сырых" SQL-запросов.
Laravel (Eloquent ORM) — PHP:
// Eloquent автоматически использует параметризацию
$user = User::where('username', $username)
->where('password', $password)
->first();
// Query Builder тоже безопасен
$user = DB::table('users')
->where('username', $username)
->where('password', $password)
->first();
Doctrine ORM — PHP (Symfony):
$user = $entityManager->getRepository(User::class)
->findOneBy([
'username' => $username,
'password' => $password
]);
// Или через DQL (Doctrine Query Language)
$query = $entityManager->createQuery(
'SELECT u FROM App\Entity\User u WHERE u.username = :username'
);
$query->setParameter('username', $username);
$user = $query->getOneOrNullResult();
SQLAlchemy — Python:
from sqlalchemy import select
from models import User
# SQLAlchemy автоматически параметризует запросы
stmt = select(User).where(
User.username == username,
User.password == password
)
user = session.execute(stmt).scalar_one_or_none()
Django ORM — Python:
from django.contrib.auth.models import User
# Django ORM всегда использует параметризацию
user = User.objects.filter(
username=username,
password=password
).first()
TypeORM — Node.js/TypeScript:
import { User } from "./entity/User";
// TypeORM автоматически защищает от инъекций
const user = await userRepository.findOne({
where: {
username: username,
password: password
}
});
Sequelize — Node.js:
const { User } = require('./models');
// Sequelize использует параметризацию по умолчанию
const user = await User.findOne({
where: {
username: username,
password: password
}
});
Prisma — Node.js/TypeScript:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Prisma обеспечивает безопасность на уровне типов
const user = await prisma.user.findFirst({
where: {
username: username,
password: password
}
});
raw queries) или конкатенации строк внутри ORM. Например: // ОПАСНО! Избегайте такого кода даже в ORM:
User::whereRaw("username = '$username'")->first(); // Laravel - уязвимо!
session.execute(text(f"SELECT * FROM users WHERE username = '{username}'")) // SQLAlchemy - уязвимо!
// ПРАВИЛЬНЫЙ пример уязвимости в Prisma:
const query = `SELECT * FROM users WHERE username = '${username}'`;
prisma.$queryRawUnsafe(query); // Prisma - УЯЗВИМО! Всегда используйте встроенные методы ORM или явную параметризацию для raw queries: // БЕЗОПАСНО в Prisma (tagged template автоматически параметризует):
prisma.$queryRaw`SELECT * FROM users WHERE username = ${username}` // Безопасно!
// БЕЗОПАСНО в Laravel:
User::whereRaw("username = ?", [$username])->first(); // Безопасно!
// БЕЗОПАСНО в SQLAlchemy:
session.execute(text("SELECT * FROM users WHERE username = :username"), {"username": username}) // Безопасно!
Принцип наименьших привилегий. Аккаунт базы данных, с которым работает приложение, должен иметь минимальные права. Если приложению не нужны операции DROP, создание таблиц или доступ к системным функциям — запретите их. Используйте отдельные учётные записи для разных типов операций (чтение, запись, администрирование).
Валидация и белый список. Для параметров, где возможны только числа или строго определённые значения, используйте проверку по шаблону или перечню допустимых значений. Это сокращает поверхность для инъекций. Например, если параметр sort_by может принимать только значения name, date, price — проверяйте его по белому списку, а не просто экранируйте.
Ограничение вывода ошибок. Не показывайте пользователю подробные сообщения об ошибках базы данных. В продакшене отправляйте в логи внутренние ошибки с полной информацией для разработчиков, а пользователю показывайте дружелюбный общий ответ типа "Произошла ошибка, попробуйте позже". Детальные ошибки помогают атакующим понять структуру базы данных.
Логирование и мониторинг. Ведите детальные логи всех SQL-запросов, исключений и подозрительной активности. Настраивайте оповещения о аномалиях: резкие всплески ошибок базы данных, подозрительные параметры в URL (содержащие UNION, SELECT, --), необычное время выполнения запросов. Быстрое обнаружение атаки может предотвратить серьёзный ущерб.
Web Application Firewall (WAF) и системы обнаружения вторжений. WAF не панацея, но он может блокировать известные шаблоны атак и значительно замедлить злоумышленника. Сочетайте WAF с поведенческим анализом и системой обнаружения вторжений (IDS/IPS). Современные WAF умеют детектировать сложные паттерны атак, но не стоит полагаться только на них — безопасный код первичен.
Безопасная конфигурация СУБД. Отключайте ненужные расширения, функции и возможности базы данных. Запретите выполнение внешних команд из SQL (например, xp_cmdshell в MS SQL), доступ к файловой системе, удалённые подключения, если они не нужны. Используйте шифрование соединений, регулярно обновляйте СУБД, применяйте патчи безопасности.
Регулярное тестирование и аудит безопасности. Проводите автоматизированные и ручные тесты на проникновение, используйте сканеры уязвимостей (например, sqlmap, Burp Suite, OWASP ZAP). Обязательно включайте code review с фокусом на места формирования SQL-строк. Проверяйте не только новый код, но и периодически аудируйте существующий — уязвимости могли быть пропущены ранее или появиться после рефакторинга.
Здесь приведены трансформации, которые помогут разработчикам заменить опасные шаблоны безопасными практиками.
❌ Плохо:
$query = "SELECT * FROM products WHERE id = " . $_GET['id'];
$result = mysqli_query($db, $query);
✅ Хорошо (PDO):
$stmt = $pdo->prepare('SELECT * FROM products WHERE id = :id');
$stmt->execute(['id' => (int)$_GET['id']]);
$result = $stmt->fetchAll();
✅ Ещё лучше (Eloquent ORM):
$product = Product::find((int)$_GET['id']);
❌ Плохо:
cursor.execute(f"SELECT * FROM products WHERE id = {product_id}")
✅ Хорошо (sqlite3):
cursor.execute("SELECT * FROM products WHERE id = ?", (product_id,))
result = cursor.fetchall()
✅ Ещё лучше (Django ORM):
product = Product.objects.get(id=product_id)
❌ Плохо:
const query = `SELECT * FROM products WHERE id = ${productId}`;
connection.query(query, (err, results) => { ... });
✅ Хорошо (mysql2):
const [rows] = await connection.execute(
'SELECT * FROM products WHERE id = ?',
[productId]
);
✅ Ещё лучше (Prisma):
const product = await prisma.product.findUnique({
where: { id: productId }
});
Заметьте явное приведение типа к целому и параметризацию. Это снимает большинство рисков.
Если есть признаки инъекции, действуйте быстро, но методично. Неправильные шаги могут уничтожить улики и затруднить расследование.
Краткое напоминание о главных шагах, которые стоит выполнить регулярно.
Разберём пару типичных мифов, которые мешают защищать приложения правильно.
Миф 1: «Если я экранирую кавычки, то всё в порядке».
Частично верно, но экранирование вручную часто делается неправильно. Параметризация надёжнее и проще в поддержке.
Миф 2: «WAF спасёт нас от всех инъекций».
Нет. WAF увеличит барьер, но не заменит безопасный код и правильную архитектуру. Настройка и обходные техники атакующих делают WAF вспомогательным уровнем.
Миф 3: «Я использую ORM, поэтому полностью защищён».
ORM значительно снижает риски, но не даёт 100% гарантии. Опасность возникает при использовании raw queries, динамических запросов или SQL-инъекций второго порядка.
Миф 4: «Данные из базы данных безопасны и не требуют проверки».
Это опасное заблуждение! SQL-инъекции второго порядка эксплуатируют именно этот миф. Всегда используйте параметризацию, даже для данных, извлечённых из БД.
SQL-инъекции остаются одной из главных угроз для веб-приложений, потому что ошибки при формировании запросов встречаются повсеместно. Хорошая новость: избежать их относительно просто, если следовать базовым правилам разработки и администрирования.
Базовая защита — параметризация (или использование ORM), минимизация прав, проверка входа, контроль ошибок и постоянный мониторинг. Если вы разрабатываете или поддерживаете сайт, начните с ревью мест, где приложение формирует SQL-строки, и постепенно уменьшайте поверхность атаки.
Помните о коварных инъекциях второго порядка — они могут обойти даже правильно настроенную защиту на этапе ввода данных. Применяйте параметризацию на всех этапах работы с данными, а не только при первоначальном вводе.
sqlmap, Burp Suite, OWASP ZAPПримечание: Эта статья предназначена исключительно для образовательных целей и повышения осведомлённости о безопасности. Использование описанных техник против систем без явного разрешения владельцев является незаконным.