Что такое SQL-инъекция и почему это опасно
SQL-инъекция — уязвимость, когда непромытые/некорректно обработанные входные данные попадают в SQL-запрос и позволяют атакующему выполнить произвольные команды в базе данных. Последствия: утечка данных, изменение/удаление данных, обход аутентификации, получение доступа к ОС через расширенные возможности СУБД.
Типы инъекций:
- In-band (error-based / union-based) — результат виден в ответе приложения.
- Blind (boolean / time-based) — ответ не даёт прямой информации, но поведение/время ответа зависит от условия.
- Out-of-band — использование вспомогательных каналов (DNS, HTTP callback).
Этические и юридические принципы тестирования
Перед любым тестированием:
- Получите письменное разрешение владельца ресурса (scope, правила, исключения).
- Работайте на тестовой среде или staging, идентичной production по стеку, но без реальных данных.
- Определите ограничения (часы тестирования, допустимые методы, закреплённый контакт на стороне заказчика).
- Логируйте всё тестирование и имейте план действий при случайного повреждения данных (backup/rollback).
Неэтичный/незаконный тест = уголовная/административная ответственность.
Защита: основная идея — недоверие к входным данным
Принцип: все внешние данные недоверенны. Комбинация мер обеспечивает надёжную защиту:
- Параметризованные запросы / подготовленные выражения (prepared statements).
- ORM с корректным использованием (без raw SQL с конкатенацией).
- Белый список валидации (validation by allowlist).
- Экранирование — вспомогательное, не как основной барьер.
- Минимизация привилегий у DB-пользователей (least privilege).
- WAF и RASP как дополнительный уровень защиты.
- Логирование и мониторинг подозрительных запросов.
- Регулярный аудит кода и ревью SQL-фрагментов.
5. Конкретные примеры защитного кода (разные языки)
PHP (PDO) — безопасный пример
<?php
// PDO с подготовленными выражениями
$dsn = 'mysql:host=localhost;dbname=appdb;charset=utf8mb4';
$pdo = new PDO($dsn, 'app_user', 'secret', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
// Пример безопасного SELECT
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE username = :username');
$stmt->execute([':username' => $username_input]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
Пояснение: ATTR_EMULATE_PREPARES => false заставляет драйвер использовать нативные подготовленные выражения.
Node.js (pg for PostgreSQL) — параметризация
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.PG_CONN });
const text = 'SELECT id, name FROM customers WHERE email = $1';
const values = [emailInput];
const res = await pool.query(text, values);
Python (psycopg2) — безопасно
import psycopg2
conn = psycopg2.connect(dsn)
with conn.cursor() as cur:
cur.execute("SELECT id, username FROM users WHERE id = %s", (user_id,))
row = cur.fetchone()
Java (JDBC) — PreparedStatement
String sql = "SELECT id, name FROM products WHERE sku = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, skuInput);
ResultSet rs = ps.executeQuery();
C# (ADO.NET)
using (var cmd = new SqlCommand("SELECT id FROM Users WHERE login = @login", conn))
{
cmd.Parameters.AddWithValue("@login", loginInput);
var reader = cmd.ExecuteReader();
}
Пример на PHP — НИКОГДА не делать (уязвимый код)
// НИКОГДА так не делайте:
$query = "SELECT * FROM users WHERE username = '" . $_GET['user'] . "'";
// Конкатенация строк — прямой путь к инъекции
Валидация входных данных — практики
- Используйте allowlist: если ожидается целое число — проверяйте
is_numericи приводите к int; если email — используйте строгую проверку формата + при желании дополнительную проверку домена. - Ограничивайте длину полей.
- Для идентификаторов и enum используйте внутренние маппинги (например, если ожидается статус: 0,1,2 — не принимайте произвольное значение).
Конфигурация БД и аккаунтов (hardening)
- Не используйте root/sa для приложения. Создайте отдельного пользователя с минимальными правами (SELECT/INSERT/UPDATE/DELETE только по нужным таблицам; избегайте DROP/ALTER/GRANT).
- Отключите небезопасные расширения в СУБД, если не используете (например, в MySQL — FILE привилегии для обычных пользователей).
- Включите шифрование соединений (TLS) между приложением и БД.
- Ограничьте IP-адреса, которые могут подключаться к БД.
- Регулярные резервные копии и проверка способности восстановить их.
WAF, RASP и другие инструменты защиты
- WAF (Web Application Firewall) — блокирует известные сигнатуры/паттерны атаки и аномалии. Используйте как слой защиты, но не полагайтесь только на него.
- RASP (Runtime Application Self-Protection) — интегрируется в приложение и может обнаруживать/блокировать инъекции на раннем этапе.
- Настройка WAF: избегайте агрессивных правил, которые ломают легитимные запросы; ставьте learning/monitoring режим прежде чем включать blocking в продакшн.
Логирование и обнаружение инцидентов
Что логировать:
- Полный текст SQL-запросов (в безопасном и GDPR-compliant виде), где это уместно.
- Входные параметры и контекст запроса (путь, IP, User-Agent, user_id если есть).
- Время выполнения запросов — медленные запросы могут указывать на попытки time-based blind SQLi.
- Ошибки СУБД: stack traces, сообщения об ошибках (error-based SQLi часто вызывает ошибку с содержимым).
Мониторинг:
- Настройте правила оповещений на аномалии: резкий рост числа ошибок 500/500-like DB errors; большое число запросов с подозрительными символами (
' OR 1=1 --и т.п.); необычные запросы к служебным таблицам. - Включите EDR/SIEM интеграцию (например, отправка логов в ELK/Graylog/Splunk).
- Используйте метрики: количество уникальных SQL ошибок по IP, количество long-running запросов, процент 4xx/5xx.
Детекция на примерах (без эксплойтов)
Примеры сигнатур/правил (логический смысл, не конкретные payloads):
- Проверка на наличие в параметрах последовательностей символов, характерных для SQL (символ одиночной кавычки
', комментарий--,/*, ключевые словаUNION,SELECTи т.п.) в сочетании с подозрительным поведением (ошибки, изменение выдачи). - Аномалия: параметры, содержащие большое количество специальных символов или цифр/символов в поле, где ожидается слово/имя (можно применить коэффициент подозрительности).
- Time-based detection: большое количество долгих запросов от одного IP/aгента.
Частые ошибки разработчиков и как их исправлять
- Использование строковой конкатенации для SQL. → Исправление: перевести на prepared statements / ORM.
- Предоставление приложению избыточных DB-привилегий. → Исправление: минимизировать привилегии; использовать отдельного read-only пользователя для операций чтения.
- Публикация подробных ошибок СУБД клиенту. → Исправление: логировать подробно на сервере, клиенту отдавать дружественное сообщение без деталей.
- Отсутствие контроля длины и типов входа. → Исправление: валидировать и правильно обрабатывать.
Практические рекомендации для CI/CD и SDLC
- Включите статический анализ кода (SAST), которые ищут небезопасную конкатенацию SQL.
- Интегрируйте DAST (динамическое тестирование) в staging, но не в prod.
- Code review: особое внимание к участкам, формирующим SQL.
- Обновления зависимостей и СУБД — патчите вовремя.
Понимание уязвимого шаблона (на примере псевдо-кода)
Ниже — псевдо пример уязвимого шаблона запроса (только для понимания механики), и безопасный эквивалент — без эксплойтов:
Уязвимый (псевдо-код, демонстрация опасности конкатенации):
-- ПРЕДСТАВЬТЕ: сервер получает userInput из формы
query = "SELECT id, email FROM users WHERE username = '" + userInput + "';"
execute(query);
Проблемма: конкатенация входных данных напрямую в SQL — вход может изменить структуру запроса.
Безопасный подход — параметризация / подготовленные выражения:
-- ПАРАМЕТРИЗОВАННЫЙ ВАРИАНТ (псевдо)
query = "SELECT id, email FROM users WHERE username = ?;"
execute_prepared(query, [userInput]);
Или в ORM: User.find({ username: userInput }) — где ORM сам параметризует.
Пять реальных примеров уязвимого кода и исправления (без эксплойтов)
Каждый пример: уязвимость (покажу паттерн), почему опасно, исправление.
ВАЖНО: в примерах я не добавляю payloads/паттерны атак — только объясняю механизмы и показываю безопасные варианты.
Пример 1 — PHP + MySQL (PDO) — уязвимость: конкатенация строк
Уязвимый паттерн (псевдо/реальный стиль):
// Уязвимый код (НЕ делайте так)
$username = $_POST['username'];
$query = "SELECT id, email FROM users WHERE username = '" . $username . "'";
$result = $pdo->query($query);
Почему уязвим: пользовательская строка встраивается напрямую — меняет структуру SQL.
Исправление (PDO, prepared statements):
// Безопасный код
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE username = :username');
$stmt->execute([':username' => $username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
Пояснение: параметры отделены от синтаксиса запроса — база воспринимает вход как значение.
Пример 2 — Node.js (pg) — уязвимость: шаблонные строки
Уязвимый паттерн:
// Уязвимо
const query = `SELECT * FROM products WHERE sku = '${req.query.sku}'`;
const res = await client.query(query);
Исправление (параметры):
const text = 'SELECT * FROM products WHERE sku = $1';
const values = [req.query.sku];
const res = await client.query(text, values);
Пояснение: placeholders ($1) предотвращают интерпретацию структуры запроса.
Пример 3 — Python (psycopg2) — уязвимость: форматирование строки
Уязвимый паттерн:
# Уязвимо
cur.execute("SELECT * FROM orders WHERE order_id = '%s'" % order_id)
Исправление:
cur.execute("SELECT * FROM orders WHERE order_id = %s", (order_id,))
Пояснение: второй аргумент параметризует значения.
Пример 4 — Java (JDBC) — уязвимость: конкатенация в query
Уязвимый:
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
Исправление:
String sql = "SELECT * FROM users WHERE email = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, email);
ResultSet rs = ps.executeQuery();
Пример 5 — C# (ADO.NET) — уязвимость: прямое строение запроса
Уязвимый:
string sql = "SELECT * FROM Clients WHERE name = '" + name + "'";
SqlCommand cmd = new SqlCommand(sql, conn);
var reader = cmd.ExecuteReader();
Исправление:
var cmd = new SqlCommand("SELECT * FROM Clients WHERE name = @name", conn);
cmd.Parameters.AddWithValue("@name", name);
var reader = cmd.ExecuteReader();
Для каждого примера: добавьте unit-test, проверки на длину строки и allowlist, если поле имеет ограниченный набор допустимых значений (например, enum или numeric id).
5. Шаблон отчёта об уязвимости (структура, можно копировать)
[Название отчёта]
Дата: YYYY-MM-DD
Исполнитель: <имя, контакт>
Заказчик: <имя организации>, контактное лицо
1. Резюме
Краткое описание проблемы, общий уровень риска (Critical/High/Medium/Low), краткое влияние.
2. Scope тестирования
URLs / приложения / версии / ограничение тестирования / стенд.
3. Методология
Кратко: manual review, SAST, DAST, тестирование в изолированной среде.
4. Найденные уязвимости (по приоритету)
4.1. [Название уязвимости]
- Описание: где обнаружена
- Компонент: файл/маршрут/метод
- Уровень риска:
- Доказательства (PoC в стенде): описание шагов, логи, скриншоты (без нанесения вреда)
- Влияние: какие данные/функции под угрозой
- Рекомендации по исправлению: конкретный код/конфигурация/практика
- Retest steps: что проверять после исправления
(повторять для каждой уязвимости)
5. Общие рекомендации
- Применить parameterized queries во всем проекте
- Минимизировать права БД
- Включить centralized logging и SIEM
- Интегрировать SAST/DAST в CI
6. План исправлений и приоритеты
- Hotfix (срочно), Medium, Long term.
7. Приложения
- Выборка логов, конфигурации, PR-патчи, ссылки на ресурсы по remediation.
