Докажите наличие уязвимости endpoint с помощью воспроизводимого поведения, специфичного для базы данных, в авторизованной тестовой среде. Поэтому привязывайте каждое значение на стороне сервера, используйте allow-list для идентификаторов и ограничивайте учётную запись базы данных.
Для GET /product?id=1 приложение должно отправлять фиксированный запрос и привязывать 1 как данные. Это правило предотвращает основную проблему со значениями. Поэтому в этом руководстве рассматривается то, что невозможно привязать, а также сохраняющиеся меры контроля ущерба.
Списки payload — плохая отправная точка. Большинство разборов SQL-инъекций идут неправильно, потому что люди классифицируют payload, а не свидетельства. Поэтому мы определим наблюдаемое поведение и проверим его на предварительно заполненной staging-цели. Затем мы исправим пути выполнения запросов, которые сделали это возможным.
SQL-инъекция уже долгое время находится среди главных предупреждений в области безопасности приложений не без причины. OWASP поставила её на первое место в Top 10 за 2013 и 2017 годы; A03:2021 Injection заняла третье место. Invicti сообщает, что CVE-2025-1094, проблема SQL-инъекции в PostgreSQL, появилась в цепочке атаки, достигшей инфраструктуры Министерства финансов США в январе 2025 года. Эта атрибуция принадлежит Invicti; она не является причиной превращать это руководство в обзор взломов.
В этой статье
- SQL-инъекция — это нарушение границы между кодом и данными
- Классифицируйте SQLi по моменту выполнения и способу наблюдения
- In-band SQLi даёт ответ в обычном response
- Blind SQLi превращает различия в response в измерение
- Тестируйте только endpoint, которыми владеете, и доказывайте меньше, чем можете
- Parameterized queries должны быть стандартом везде
- Для dynamic SQL нужны allow-list, а не хитрое экранирование
- Используйте release checklist, охватывающий код и ущерб
SQL-инъекция — это нарушение границы между кодом и данными
Уязвимый endpoint может превратить этот запрос:
GET /product?id=1
в:
SELECT name, price FROM products WHERE id = '1'
Если приложение конкатенирует входные данные, синтаксис SQL из запроса может изменить структуру запроса. Таким образом, база данных получает инструкции приложения и ненадёжный текст в одной команде.
Безопасная форма выглядит так:
SELECT name, price FROM products WHERE id = ?
Driver привязывает 1 отдельно. Поэтому такое значение, как tom' or '1'='1, остаётся литеральным именем пользователя, а не превращается в новое условие. Памятка OWASP по предотвращению SQL-инъекций формулирует основное правило: prepared statements не позволяют атакующему изменить смысл запроса.
Учитывайте три решения. Привязывайте значения через server-side API базы данных. Сопоставляйте динамические идентификаторы с фиксированными фрагментами SQL. Приложению нужны только используемые им разрешения.
Если ваше исправление — «экранировать кавычку», вы не исправили SQL-инъекцию; вы выбрали более хрупкий способ продолжать писать SQL.
Классифицируйте SQLi по моменту выполнения и способу наблюдения
Классифицируйте уязвимость по двум осям: когда входные данные становятся опасными и как вы наблюдаете результат.
- Classic, или in-band, SQLi возвращает результаты базы данных или ошибки базы данных в обычном HTTP response.
- Blind SQLi скрывает результат, поэтому вы делаете вывод об условии по изменившейся странице, статусу, длине response, ошибке или времени выполнения.
- Out-of-band SQLi заставляет базу данных выполнить внешний запрос, обычно DNS, который вы наблюдаете отдельно.
- Second-order SQLi сохраняет входные данные во время одной операции, а затем повторно использует их в уязвимом запросе. Это описывает время выполнения, поэтому пересекается с указанными выше категориями обратной связи.
Для /product?id=1 изучите, что меняется при изменении входных данных. Показывает ли response дополнительные строки или ошибку? Даёт ли истинное условие другое содержимое по сравнению с ложным условием? Могут ли внешне эквивалентные запросы также показывать контролируемую задержку?
Изменившийся response — это индикатор. Подтверждение требует воспроизводимости, а также объяснения, связанного с конкретной базой данных или основанного на телеметрии, желательно воспроизведённого в контролируемой лаборатории.
Это различие важно. Scanner может заметить подозрительную разницу, не доказывая, что синтаксис SQL достиг базы данных.
In-band SQLi даёт ответ в обычном response
Classic, или in-band, SQLi — самый простой для доказательства класс и самый простой для чрезмерного тестирования. Полезная обратная связь поступает в обычном HTTP response — либо в виде изменившихся результатов, либо в виде ошибки базы данных.
Пример tracking-cookie от PortSwigger использует response с сообщением “Welcome back”, чтобы показать, изменило ли условие лежащий в основе запрос. В собственной lab controlled test может заставить endpoint вернуть дополнительную намеренно добавленную synthetic row. Доказательством является воспроизводимая связь между входными данными и результатом, а не количество строк, которые вы можете получить.
Поведение на основе ошибок — ещё один канал response. Приложение может вернуть:
Unterminated string literal started at position 52 in SQL SELECT * FROM tracking WHERE id = '''. Expected char
Это сообщение раскрывает структуру запроса и поведение базы данных. Преднамеренно заполненная lab также может продемонстрировать ошибки преобразования с помощью:
CAST((SELECT example_column FROM example_table) AS int)
Не адаптируйте это для реальных учётных данных или production-данных. Оставляйте запрос внутри одноразовой среды, содержащей значения, созданные специально для теста. Некоторые конфигурации базы данных и ошибок выводят преобразованное значение; многие production-конфигурации его подавляют.
Возвращайте пользователям общий идентификатор ошибки. Храните подробную диагностику в защищённых логах и избегайте записи конфиденциальных значений запросов или ненужного текста запросов. Скрытие ошибки убирает свидетельство; оно не исправляет запрос.
Blind SQLi превращает различия в response в измерение
Когда response скрывает результаты запроса, проверьте, создаёт ли контролируемое условие воспроизводимый сигнал. В авторизованной staging-среде с намеренно добавленным marker сравните подходящие для базы данных истинные и ложные условия. Иллюстративный шаблон PortSwigger выглядит так:
xyz' AND '1'='1
xyz' AND '1'='2
Точный синтаксис зависит от контекста запроса и базы данных. Не вставляйте это в системы, которыми вы не владеете.
Условие SUBSTRING может проверить позицию символа, а сравнение вроде > 'm' сужает известный synthetic marker с помощью бинарного поиска. Используйте это только для установления контроля над предварительно заполненным значением. Остановитесь, как только marker докажет, что условие достигает базы данных.
| Техника | Сигнал | Остановитесь, когда |
|---|---|---|
| Boolean-based | Стабильное различие между истинными и ложными условиями | Различие повторяется, а телеметрия подтверждает причину |
| Error-based | Условная ошибка базы данных | Корреляция ошибки установлена в lab |
| Time-based | Контролируемое различие задержки | Время отделяется от baseline, а телеметрия подтверждает это |
| Out-of-band | Внешнее взаимодействие с уникальным идентификатором | Callback коррелирует с тестовым путём |
Условные ошибки могут использовать выражение CASE:
CASE WHEN <condition> THEN 1/0 ELSE 'a' END
Этот сигнал исчезает, когда приложение обрабатывает все ошибки базы данных одинаково.
Для timing-тестов нужен baseline. Сначала измерьте обычные запросы, запишите их распределение, затем используйте короткую контролируемую задержку и минимально повторите сравнение.
| База данных | Синтаксис задержки |
|---|---|
| Microsoft SQL Server | WAITFOR DELAY |
| MySQL | SLEEP() |
| PostgreSQL | pg_sleep() |
Синтаксис зависит от базы данных. Сетевая нагрузка, caching, состояние session и конкуренция за ресурсы базы данных также могут менять latency. Я не могу назвать различие latency SQLi, основываясь только на HTTP response; измерьте его относительно baseline и свяжите с телеметрией базы данных или приложения.
Out-of-band тестирование безопасности приложений, часто называемое OAST, помогает, когда response действительно неинформативен. Руководство PortSwigger по blind SQL injection описывает внешнее взаимодействие как полезный сигнал в такой ситуации. DNS callback с уникальным идентификатором может подтвердить внешнее взаимодействие, но прежде чем назвать endpoint уязвимым, свяжите его с контролируемым запросом и путём приложения.
Тестируйте только endpoint, которыми владеете, и доказывайте меньше, чем можете
Используйте эти проверки только против систем, которыми вы владеете или на тестирование которых у вас есть явное разрешение. Предпочитайте локальную или staging-среду с synthetic records, свежей резервной копией, rate limits, мониторингом и явно определённым правилом остановки.
Для /product?id=1 сначала соберите несколько обычных запросов. Затем сравните два авторизованных условия, соответствующих базе данных, для одной и той же предварительно заполненной записи:
GET /product?id=<true-condition-for-the-staged-query>
GET /product?id=<false-condition-for-the-staged-query>
Приведённые выше placeholders намеренны. Корректный синтаксис зависит от того, как приложение заключает значение в кавычки и какая база данных его выполняет. Создайте пару в lab, а не из общего списка payload.
Сравните статус и длину body. Также проверьте стабильные markers и latency. Отдельно проверьте логи базы данных и correlation IDs. Здесь важно сделать оговорку о тестировании: состояние cache, sessions, retries и нагрузка на backend могут вызывать различия response, не связанные с SQL-инъекцией. Считайте такие различия индикаторами, пока контролируемое воспроизведение или объяснение на основе телеметрии не подтвердит причину.
Используйте инструменты для разных задач:
| Инструмент или категория | Лучшее применение | Ограничение |
|---|---|---|
| Burp Suite или OWASP ZAP | Повторная отправка запросов и ручная проверка | Требует интерпретации; индикаторы не являются доказательством |
sqlmap | Прицельное подтверждение в авторизованном тесте | Нужно указать точку инъекции; он не заменяет исследование flow или анализ second-order случаев |
| Enterprise DAST | Повторяемое сканирование в CI/CD | Менее гибок для custom exploitation |
| Лёгкие scanners, такие как Nikto, SQLiv или Wapiti | Широкая reconnaissance | Могут давать false positives |
| Template scanners, такие как Nuclei | Крупномасштабное обнаружение на основе templates | Обнаружение не устанавливает exploitability |
Для designated staging-параметра используйте консервативное обнаружение:
sqlmap -u "https://staging.example.test/product?id=1" \
--batch --level=1 --risk=1
Это намеренно не использует --dbs; для доказательства инъекции не требуется перечислять базу данных. Не превращайте release check в упражнение по извлечению данных.
Результат scanner — это зацепка, а не отчёт о взломе. Сложные многоэтапные flow и second-order случаи по-прежнему требуют code review, integration tests и ручной трассировки.
Parameterized queries должны быть стандартом везде
Порядок защиты OWASP обоснован: prepared statements, безопасно реализованные stored procedures, allow-list validation для значений, которые нельзя привязать, и экранирование как настоятельно не рекомендуемый запасной вариант.
Java endpoint может разобрать и привязать идентификатор следующим образом:
int productId;
try {
productId = Integer.parseInt(request.getParameter("id"));
} catch (NumberFormatException ex) {
response.sendError(400, "Invalid product id");
return;
}
PreparedStatement ps = connection.prepareStatement(
"SELECT name, price FROM products WHERE id = ?"
);
ps.setInt(1, productId);
ResultSet results = ps.executeQuery();
Разбор улучшает проверку типа. Привязка не позволяет значению стать синтаксисом SQL. Нужны оба механизма.
PHP с PDO может использовать именованный параметр:
$stmt = $dbh->prepare(
"SELECT name, price FROM products WHERE id = :id"
);
$stmt->bindValue(':id', $productId, PDO::PARAM_INT);
if (!$stmt->execute()) {
throw new RuntimeException('Database query failed');
}
$results = $stmt->fetchAll();
ORM не является границей безопасности. Его parameterized API может быть безопасным; его escape hatch для raw-query по-прежнему остаётся raw SQL. Проверяйте вызовы с именами raw, execute, fromSql или похожими, а также интерполяцию строк внутри query builders.
Тот же принцип применяется к Hibernate named parameters, условиям ActiveRecord и эквивалентным API в C# и Rust. Само по себе имя метода bind ничего не доказывает. Проследите один запрос через driver или integration test и определите, получает ли граница базы данных отдельные параметры или сконкатенированный raw query. Некоторые библиотеки эмулируют prepared statements или предлагают режимы, меняющие способ подготовки; требование заключается в том, чтобы пользовательские данные не могли изменить синтаксис SQL.
Placeholders привязывают значения. Как правило, они не могут привязать имя таблицы, имя столбца или направление сортировки. Для структурных изменений нужен allow-list.
Для dynamic SQL нужны allow-list, а не хитрое экранирование
Если отчёт принимает name или price в качестве поля сортировки, сопоставьте эти варианты с constants:
String column;
switch (sortField) {
case "name":
column = "name";
break;
case "price":
column = "price";
break;
default:
throw new IllegalArgumentException("Unsupported sort field");
}
String direction = descending ? "DESC" : "ASC";
String sql =
"SELECT name, price FROM products ORDER BY "
+ column + " " + direction;
Каждый фрагмент SQL поступает из исходного кода. Пользовательский ввод выбирает вариант; он никогда не становится синтаксисом SQL.
Применяйте то же правило к выбору таблиц и столбцов. OWASP предупреждает, что управляемый пользователем выбор целевых таблиц или столбцов указывает на неудачную архитектуру и может оправдывать переписывание. Если пользователи могут выбирать произвольную структуру базы данных, изучите эту границу авторизации и схемы, прежде чем добавлять ещё одну функцию экранирования.
Stored procedures могут быть столь же эффективны, как prepared statements, если используют фиксированный SQL и типизированные параметры. Dynamic SQL внутри procedure требует той же дисциплины, что и dynamic SQL в коде приложения. Ниже приведён соответствующий вызов внутри существующей procedure SQL Server, где @UserID и @Dept уже переданы как переменные procedure:
DECLARE @sql NVARCHAR(200);
SELECT @sql =
N'SELECT balance FROM accounts_table
WHERE user_ID = @UID AND department = @DPT';
EXEC sp_executesql
@sql,
N'@UID NVARCHAR(20), @DPT NVARCHAR(10)',
@UID = @UserID,
@DPT = @Dept;
Значения привязаны. Интерполированные идентификаторы по-прежнему требуют фиксированного сопоставления.
Эти procedures не становятся автоматически безопаснее: если приложение выполняет их от имени db_owner, успешная компрометация всё равно начинается с полного доступа к базе данных. Изучите фактический execution context, права procedure, ownership chaining и поведение impersonation. Для SQL Server проверьте procedures на наличие шаблонов dynamic execution, таких как sp_execute, execute и exec.
Используйте release checklist, охватывающий код и ущерб
Parameterization устраняет центральную проблему границы между кодом и данными. Release review должна доказать, что это правило охватывает каждый путь выполнения запросов и что отдельная ошибка не может предоставить web process всю базу данных.
- Привязывайте каждое значение на стороне сервера. Проследите входные данные запроса через repositories и query builders. Проверьте также background jobs. Проверьте также stored procedures. Убедитесь, что пользовательские данные не могут изменить синтаксис SQL.
- Сопоставляйте каждый структурный выбор. Имена таблиц, имена столбцов и
ASCилиDESCдолжны поступать из фиксированных server-side вариантов. Применяйте на сервере проверку типа, длины, диапазона и конечного набора вариантов. - Находите escape hatch для raw SQL. Проверьте методы ORM, интерполированные строки, migrations, report builders и helper functions базы данных. Не делайте вывод о безопасности на основании метода с именем
bind. - Проверяйте привилегии stored procedure. Подтвердите типизированные параметры, безопасный dynamic SQL и узко ограниченные права выполнения. Production account приложения не должен быть
db_owner. - Ограничивайте и отслеживайте сбои. Возвращайте общие внешние ошибки и защищайте подробные логи. Настройте alerts на повторяющиеся ошибки базы данных и необычный объём запросов. Также отслеживайте неожиданные patterns доступа.
- Воспроизведите одно безопасное доказательство. В staging используйте намеренно добавленный marker и запишите запрос, response, телеметрию и точку остановки. Не используйте в тесте credentials, tokens и production records.
Если вы не можете проследить значение до server-side bind и воспроизвести безопасное staging-доказательство, отложите release.