SQL-инъекция — одна из старейших и при этом до сих пор актуальных уязвимостей. По данным OWASP, она стабильно входит в топ-3 угроз для веб-приложений. Понимать, как она работает, важно не только для разработчиков, но и для всех, кто работает с базами данных.
Классический пример: обход аутентификации
Представьте форму входа. Бэкенд формирует SQL-запрос конкатенацией строк:
# ОПАСНЫЙ КОД — никогда так не делайте
username = request.POST['username']
password = request.POST['password']
query = f"""
SELECT id FROM users
WHERE username = '{username}'
AND password = '{password}'
"""
Если пользователь вводит в поле username значение:
admin' --
Итоговый запрос становится:
SELECT id FROM users
WHERE username = 'admin' --' AND password = '...'
Символы -- — комментарий в SQL. Всё после них игнорируется. Условие проверки пароля исчезает, и атакующий входит как admin без знания пароля.
UNION-based инъекция: кража данных
Более опасный вариант — UNION-инъекция, позволяющая извлекать данные из произвольных таблиц:
-- Уязвимый запрос для поиска товаров
-- /search?q=phone
SELECT name, price FROM products WHERE name LIKE '%phone%'
-- Атакующий вводит:
-- /search?q=phone' UNION SELECT username, password FROM users --
-- Итоговый запрос:
SELECT name, price FROM products WHERE name LIKE '%phone%'
UNION
SELECT username, password FROM users --'
В ответе API вместо товаров вернутся логины и пароли всех пользователей. Если пароли хранятся в открытом виде — катастрофа.
Защита: параметризованные запросы
Единственно надёжный способ защиты — параметризованные запросы (parameterized queries / prepared statements). Данные пользователя передаются отдельно от SQL-кода и никогда не интерпретируются как часть запроса.
Python (psycopg2)
import psycopg2
conn = psycopg2.connect("dbname=mydb user=app")
cur = conn.cursor()
# ПРАВИЛЬНО: параметры передаются отдельно
username = request.POST['username']
password = request.POST['password']
cur.execute(
"SELECT id FROM users WHERE username = %s AND password = %s",
(username, password) # psycopg2 экранирует значения
)
user = cur.fetchone()
Даже если username = "admin' --", psycopg2 экранирует кавычку и передаст её как литеральный символ, а не как часть SQL.
Node.js (pg)
const { Pool } = require('pg');
const pool = new Pool();
// ПРАВИЛЬНО: параметризованный запрос
async function getUser(username, password) {
const result = await pool.query(
'SELECT id FROM users WHERE username = $1 AND password = $2',
[username, password] // $1, $2 — позиционные параметры
);
return result.rows[0];
}
// ОПАСНО: конкатенация
async function getUserUnsafe(username, password) {
const result = await pool.query(
`SELECT id FROM users WHERE username = '${username}'`
);
return result.rows[0];
}
Prepared Statements в PostgreSQL
-- Подготовленный запрос на уровне сессии
PREPARE get_user (text, text) AS
SELECT id, username FROM users
WHERE username = $1 AND password_hash = crypt($2, password_hash);
-- Выполнение с параметрами
EXECUTE get_user('admin', 'my_password');
-- Освобождение
DEALLOCATE get_user;
Принцип наименьших привилегий
Параметризация защищает от инъекций, но не отменяет важность правильной настройки прав доступа. Приложение должно работать от пользователя с минимально необходимыми правами:
-- Создаём отдельного пользователя для приложения
CREATE USER app_user WITH PASSWORD 'secure_password';
-- Даём только нужные права, не суперпользователя
GRANT SELECT, INSERT, UPDATE ON orders TO app_user;
GRANT SELECT ON products TO app_user;
GRANT SELECT ON users TO app_user;
-- НЕ даём права на удаление и DDL
-- REVOKE DELETE ON orders FROM app_user;
-- REVOKE ALL ON pg_user FROM app_user; -- и так нет, но явно зафиксируем
-- Проверить текущие права
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'app_user'
ORDER BY table_name;
Если атакующий всё же найдёт инъекцию, он сможет делать только то, что разрешено app_user — например, читать заказы, но не удалять таблицы и не читать системные каталоги.
Мониторинг подозрительной активности
-- Найти запросы с подозрительными паттернами в pg_stat_activity
-- (для активных сессий прямо сейчас)
SELECT
pid,
usename,
application_name,
client_addr,
now() - query_start AS duration,
left(query, 200) AS query
FROM pg_stat_activity
WHERE query ILIKE '%union%select%'
OR query ILIKE '%--%'
OR query ILIKE '%information_schema%'
OR query ILIKE '%pg_tables%';
Что ещё помогает
Хранимые процедуры сами по себе не защищают от инъекций, если внутри них тоже конкатенируются строки. Но правильно написанная хранимая процедура ограничивает интерфейс взаимодействия с базой.
ORM (SQLAlchemy, Django ORM, Hibernate) автоматически используют параметризованные запросы для стандартных операций. Опасность возникает при использовании raw() / execute() с конкатенацией строк.
WAF (Web Application Firewall) может блокировать типичные инъекционные паттерны, но не является заменой параметризации — это дополнительный слой защиты.
Итог
| Метод защиты | Надёжность | Примечание |
|---|---|---|
| Параметризованные запросы | Высокая | Основной метод |
| Prepared statements | Высокая | Аналог, работает на уровне БД |
| Наименьшие привилегии | Средняя | Ограничивает последствия |
| ORM | Высокая | При правильном использовании |
| Экранирование строк вручную | Низкая | Ненадёжно, избегайте |
SQL-инъекции предотвращаются на уровне кода, а не конфигурации. Параметризованные запросы — не опция, а обязательное требование для любого production-приложения.
SQL-инъекции — частая тема на технических собеседованиях. Подготовьтесь к ним в разделе Подготовка к собесу или отработайте запросы в тренажёре.