Как предотвратить SQL-инъекцию и безопасно её тестировать

Mon Aug 17 2026

Докажите наличие уязвимости 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-инъекция — это нарушение границы между кодом и данными

Уязвимый 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 ServerWAITFOR DELAY
MySQLSLEEP()
PostgreSQLpg_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 всю базу данных.

  1. Привязывайте каждое значение на стороне сервера. Проследите входные данные запроса через repositories и query builders. Проверьте также background jobs. Проверьте также stored procedures. Убедитесь, что пользовательские данные не могут изменить синтаксис SQL.
  2. Сопоставляйте каждый структурный выбор. Имена таблиц, имена столбцов и ASC или DESC должны поступать из фиксированных server-side вариантов. Применяйте на сервере проверку типа, длины, диапазона и конечного набора вариантов.
  3. Находите escape hatch для raw SQL. Проверьте методы ORM, интерполированные строки, migrations, report builders и helper functions базы данных. Не делайте вывод о безопасности на основании метода с именем bind.
  4. Проверяйте привилегии stored procedure. Подтвердите типизированные параметры, безопасный dynamic SQL и узко ограниченные права выполнения. Production account приложения не должен быть db_owner.
  5. Ограничивайте и отслеживайте сбои. Возвращайте общие внешние ошибки и защищайте подробные логи. Настройте alerts на повторяющиеся ошибки базы данных и необычный объём запросов. Также отслеживайте неожиданные patterns доступа.
  6. Воспроизведите одно безопасное доказательство. В staging используйте намеренно добавленный marker и запишите запрос, response, телеметрию и точку остановки. Не используйте в тесте credentials, tokens и production records.

Если вы не можете проследить значение до server-side bind и воспроизвести безопасное staging-доказательство, отложите release.