SQL Injection — тип атаки на базу данных приложения, при которой злоумышленник внедряет вредоносный SQL-код в параметры запроса, получая несанкционированный доступ к данным или возможность их модификации. По данным OWASP (2025), SQL Injection остаётся одной из критических уязвимостей, способной привести к полной компрометации базы данных. Инъекция SQL-кода позволяет злоумышленнику читать, изменять и удалять записи, а в некоторых случаях — получить доступ к операционной системе сервера.
Главное
SQL Injection — это уязвимость, возникающая, когда приложение формирует SQL-запрос путём конкатенации строк с пользовательскими данными. Злоумышленник передаёт в параметр запроса специально сконструированную строку, которая изменяет структуру SQL-команды. Вместо того чтобы быть данными, введённая строка становится частью SQL-кода, что позволяет атакующему выполнять произвольные запросы к базе данных. По данным Verizon Data Breach Report (2025), SQL Injection присутствует в 8% всех исследованных утечек данных.
Несмотря на известность уязвимости (первые упоминания — конец 1990-х), SQL Injection встречается в современных приложениях. Причина — человеческий фактор: разработчики допускают конкатенацию строк в коде, legacy-код не рефакторится, а ORM-фреймворки используются неправильно (например, raw-запросы с подстановкой значений через строки). По данным Veracode (2026), около 14% всех просканированных приложений содержат хотя бы одну SQLi-уязвимость.
Успешная SQL Injection даёт атакующему широкий спектр возможностей: чтение любых таблиц базы данных, включая хеши паролей и персональные данные пользователей; модификация и удаление записей; выполнение административных операций (DROP TABLE, TRUNCATE); в некоторых конфигурациях — удалённое выполнение команд через xp_cmdshell (MSSQL) или INTO OUTFILE (MySQL). Последствия — от утечки пользовательских данных до полной потери контроля над системой.
SQL-инъекции классифицируются по способу извлечения данных из базы. Выбор метода зависит от того, как приложение обрабатывает результаты запроса и сообщения об ошибках. Классификация OWASP выделяет три основных типа: In-band (данные извлекаются через тот же канал), Inferential/Blind (логические выводы) и Out-of-band (данные передаются через другой канал).
| Тип | Способ извлечения данных | Сложность | Частота |
|---|---|---|---|
| In-band (классический) | Непосредственно через результат запроса | Низкая | Высокая |
| Blind SQLi | Логические выводы по ответам сервера | Высокая | Средняя |
| Out-of-band | Через внешний канал (DNS, HTTP) | Средняя | Низкая |
Самый распространённый тип. Злоумышленник внедряет SQL-код в параметр запроса, и результат внедрения виден непосредственно в ответе сервера. Два подвида: Error-based (через сообщения об ошибках БД) и UNION-based (через оператор UNION SELECT). Error-based использует информацию из сообщений об ошибках, например, синтаксическую ошибку MySQL, которая может раскрыть имя таблицы или структуру запроса. UNION-based позволяет объединить результаты легитимного запроса с данными из других таблиц базы данных.
Используется, когда приложение не отображает результаты запроса или сообщения об ошибках. Злоумышленник задаёт вопросы типа "да/нет", отправляя запросы с логическими условиями и анализируя разницу в ответе сервера (например, по времени отклика или содержимому страницы). Time-based Blind SQLi использует функцию задержки (SLEEP, WAITFOR DELAY) для подтверждения условий — если страница загружается дольше, условие истинно. Этот метод очень медленный — извлечение одной записи может занять часы.
# Пример Blind SQL Injection (time-based)
# Если SQLi уязвима, SLEEP(2) выполняется при условии
import requests
import time
payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload
start = time.time()
response = requests.get(url)
elapsed = time.time() - start
# Если ответ пришёл через >2 секунды — первая буква пароля 'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")
Данные передаются не через HTTP-ответ, а через альтернативные каналы: DNS-запросы, HTTP-запросы на внешний сервер, SMTP. Используется, когда приложение не возвращает результаты запроса и не показывает ошибок. MySQL поддерживает функцию LOAD_FILE(), которая может инициировать DNS-запрос, а MSSQL — xp_dirtree для отправки данных на удалённый SMB-сервер. Out-of-band SQL Injection эффективен, но требует дополнительных настроек на сервере злоумышленника и наличия определённых функций БД.
Механизм SQL Injection основан на том, что SQL использует кавычки для строковых литералов. Если приложение подставляет пользовательский ввод непосредственно в SQL-запрос без экранирования, злоумышленник может "закрыть" строку и добавить произвольный SQL-код. Например, в запросе SELECT * FROM users WHERE name = '$input' подстановка ' OR '1'='1 превращает запрос в SELECT * FROM users WHERE name = '' OR '1'='1', что возвращает всех пользователей.
Рассмотрим форму логина с запросом SELECT * FROM users WHERE username = '$user' AND password = '$pass'. Если злоумышленник вводит в поле username admin' -- , а пароль оставляет пустым, результирующий запрос становится SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''. Символы -- комментируют остаток запроса, и условие проверки пароля отключается. Сервер возвращает запись пользователя admin, и злоумышленник входит в систему без знания пароля.
# Пример SQL Injection — обход аутентификации
# УЯЗВИМЫЙ КОД: прямая конкатенация строк
def login_vulnerable(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)
return cursor.fetchone() is not None
# username = "admin' --" обнуляет проверку пароля
# БЕЗОПАСНАЯ ВЕРСИЯ: параметризованный запрос
def login_secure(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
return cursor.fetchone() is not None
Оператор UNION SELECT позволяет объединить результаты двух SELECT-запросов. Если злоумышленник находит уязвимый параметр, он добавляет UNION SELECT с запросом из другой таблицы. Например: ' UNION SELECT username, password FROM admins --. Условие успеха — количество колонок в обоих запросах должно совпадать. Количество колонок определяется через ORDER BY (подстановка ' ORDER BY 1--, затем 2, 3... до ошибки). Зная количество колонок, злоумышленник подставляет UNION SELECT с таким же числом полей.
Мобильные приложения сталкиваются с SQL Injection как на серверной стороне (API), так и на клиенте — в локальных базах данных (SQLite, Realm). Хотя риск серверной SQLi в мобильном API аналогичен веб-приложениям, локальная БД создаёт дополнительный вектор. Если приложение хранит данные в SQLite и выполняет запросы с конкатенацией строк, вредоносные данные, попавшие в локальную БД через API, могут вызвать SQLi при последующей обработке.
Локальная SQLite-база на устройстве также уязвима к SQL Injection, если приложение формирует запросы конкатенацией строк. Content Providers на Android и Core Data на iOS используют параметризацию по умолчанию, но raw-запросы требуют внимания разработчика. SQLite не поддерживает множественные запросы через точку с запятой, что ограничивает возможности злоумышленника, но не защищает от чтения данных через WHERE-условия. Всегда используйте selectionArgs в Android и NSPredicate с параметрами в iOS.
API, к которому обращается мобильное приложение, уязвим так же, как любой веб-сервер. Мобильные разработчики часто считают, что SQLi — проблема только бэкенда, но уязвимость возникает на уровне API-эндпоинта, который принимает параметры от клиента. Разделение ответственности не защищает: если бэкенд-разработчик забыл параметризовать запрос, мобильное приложение пользователя становится вектором атаки. Требуйте от бэкенда использования ORM или prepared statements.
// Пример SQL Injection в локальной SQLite на Android
// УЯЗВИМЫЙ КОД: прямая конкатенация
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = $userId", null
)
}
// БЕЗОПАСНАЯ ВЕРСИЯ: параметризация через selectionArgs
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
}
Защита от SQL Injection строится на простом принципе: никогда не доверяйте пользовательскому вводу в SQL-запросах. Единственный надёжный метод — параметризация запросов (prepared statements), при которой SQL-код и данные передаются отдельно. Все остальные методы — экранирование, валидация, WAF — являются дополнительными слоями защиты, но не заменяют параметризацию. По данным OWASP (2025), параметризация предотвращает 100% SQL Injection-атак.
При использовании prepared statements SQL-запрос сначала компилируется сервером БД без данных, а затем значения параметров передаются отдельно. База данных рассматривает параметры как данные, а не как исполняемый код. Даже если злоумышленник передаст ' OR '1'='1, БД интерпретирует это как строковое значение, а не как SQL-код. Prepared statements поддерживаются всеми современными языками и фреймворками: PDO в PHP, PreparedStatement в Java, cursor.execute в Python.
Современные ORM (Hibernate, Entity Framework, SQLAlchemy, Room) автоматически используют параметризацию при выполнении запросов, если разработчик не переходит на raw-запросы. Однако ORM не защищают полностью: конструкции вроде @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true) в JPA требуют передачи параметров через named parameters, а не конкатенации. Query Builders (Knex, jOOQ) также параметризуют запросы по умолчанию, если не использовать raw-методы.
Экранирование спецсимволов (mysql_real_escape_string) — устаревший метод, который не защищает от всех типов SQL Injection. Проблема: экранирование зависит от кодировки и может быть обойдено при использовании многобайтовых кодировок (например, GBK в азиатских системах). Используйте экранирование только в legacy-коде, где параметризация невозможна, и всегда в комбинации с строгой валидацией типа входных данных.
| Метод | Эффективность | Рекомендация |
|---|---|---|
| Prepared Statements | 100% | Обязательно для всех запросов |
| ORM (правильное использование) | 99% | Рекомендуется |
| Экранирование строк | 70% (зависит от кодировки) | Только legacy |
| Валидация ввода (white-list) | 50% (только для чисел) | Дополнительно |
| WAF (Web Application Firewall) | 60% | Дополнительно |
Учётная запись приложения должна иметь минимально необходимые права: SELECT, INSERT, UPDATE, DELETE — только на те таблицы, которые реально требуются приложению. Запретите использование DROP, TRUNCATE, CREATE для прикладной учётной записи. Это ограничит ущерб даже в случае успешной SQL Injection: злоумышленник не сможет удалить таблицы или выполнить административные операции.
Регулярное тестирование на SQL Injection должно быть частью пайплайна безопасной разработки. Комбинация статического анализа, динамического сканирования и ручного пентеста даёт наилучшие результаты. По данным Synopsys Cybersecurity Report (2025), автоматические сканеры находят до 70% SQLi-уязвимостей, однако сложные Blind-атаки требуют ручного тестирования.
Для мобильных приложений важен и анализ локального SQLite: проверьте все rawQuery-вызовы, ContentProvider-запросы и Room-запросы с rawQuery. Инструменты: Android Studio Lint (детектирует SQLi в SQLite), MobSF (Mobile Security Framework) для автоматического статического и динамического анализа APK/IPA. Рекомендуется также тестировать API-эндпоинты через sqlmap с прокси-перехватом трафика мобильного приложения.
Часто задаваемые вопросы
SQL Injection атакует реляционные базы данных через SQL-запросы. NoSQL Injection воздействует на нереляционные БД (MongoDB, Couchbase) через их операторы запросов ($gte, $ne, $where). В MongoDB инъекция возможна, если приложение формирует BSON-документ из строки JSON. Механизмы защиты аналогичны: параметризация и валидация типов.
Найдите все места, где SQL-запросы формируются через конкатенацию строк с пользовательскими данными. Ищите паттерны вида "SELECT ... WHERE id = " + userId или f"UPDATE ... SET name = '{name}'". Каждая такая строка — потенциальная SQL-инъекция. Замените все на параметризованные запросы или prepared statements.
ORM-фреймворки защищают автоматически, только если вы используете их методы Query Builder и named parameters. Если вы используете raw-запросы (nativeQuery в JPA, rawQuery в Room), защита не работает — необходимо передавать параметры через подготовленные выражения, а не через конкатенацию строк.
Second-Order SQL Injection — атака, при которой вредоносные данные сохраняются в БД как безопасные, но затем используются в другом запросе без экранирования. Например, злоумышленник регистрируется с username ' OR '1'='1. Данные сохраняются как строка — при регистрации атаки нет. Но если другой запрос подставляет username в SQL без параметризации, инъекция срабатывает.
NoSQL Injection потенциально опаснее из-за меньшей осведомлённости разработчиков. Разработчики знают об SQL Injection и большинство используют ORM, но о NoSQL Injection знают немногие. В MongoDB неправильно сформированный запрос может вернуть все документы коллекции. Защита — те же prepared statements (BSON-параметризация) и строгая валидация входных данных.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также