Как защититься от SQL-инъекций: параметризованные запросы и ORM
Практика4 минРедакция Innovialabs
Коротко
От SQL-инъекций защищают параметризованные запросы: текст запроса и данные передаются в базу отдельно, и ввод не может стать командой. Имена колонок берут из белого списка, а приложение работает под пользователем БД с минимальными правами.
Главное правило
Никогда не собирайте SQL-запрос склейкой строк с данными. Используйте параметризованные запросы (prepared statements): в запросе стоят плейсхолдеры, а значения передаются отдельно. База сначала разбирает структуру запроса и только потом подставляет данные — как значения, а не как код.
Экранирование кавычек вручную, фильтры «опасных слов» и проверки на фронтенде — не защита.
Примеры на разных языках
PHP (PDO)
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
Python (psycopg)
cur.execute(
"SELECT id, name FROM users WHERE email = %s",
(email,),
)
%s здесь — плейсхолдер драйвера. Ошибка — подставить значение самому через f-строку или оператор %.
Node.js (pg)
const { rows } = await pool.query(
"SELECT id, name FROM users WHERE email = $1",
[email]
);
Ловушки ORM
ORM формирует параметризованные запросы сам, но почти в каждом есть «сырой» режим, где защита на вас.
| ORM | Безопасно | Опасно |
|---|---|---|
| Prisma | $queryRaw`... ${email}` (тегированный шаблон) | $queryRawUnsafe со строкой из данных |
| Django | .raw("... %s", [email]) | .raw(f"... {email}"), .extra() с данными |
| SQLAlchemy | text("... :email") с параметрами | f-строка внутри text() |
| Laravel | whereRaw('email = ?', [$email]) | DB::raw("... $email") |
| Sequelize | query(sql, { replacements }) | шаблонная строка с данными |
Ищите в коде слова raw, Unsafe, execute рядом с интерполяцией строк — это первые кандидаты на ревью.
Динамический ORDER BY
Плейсхолдеры работают только для значений. Имя колонки или направление сортировки параметром передать нельзя, поэтому используйте белый список:
const SORT = { name: "name", date: "created_at", price: "price" };
const column = SORT[req.query.sort] ?? "created_at";
const dir = req.query.dir === "asc" ? "ASC" : "DESC";
const sql = `SELECT id, name FROM products ORDER BY ${column} ${dir} LIMIT $1`;
await pool.query(sql, [20]);
Пользователь выбирает ключ, а в запрос попадает только заранее известная строка. Так же поступают с именами таблиц и списком колонок.
Поиск через LIKE
Шаблон тоже передаётся параметром. Дополнительно экранируйте % и _ во вводе, иначе пользователь сможет подставлять свои маски и нагружать базу:
const term = input.replace(/[\\%_]/g, "\\$&");
await pool.query(
"SELECT id, name FROM products WHERE name ILIKE $1",
[`%${term}%`]
);
В PostgreSQL и MySQL обратный слеш — escape-символ для LIKE по умолчанию; в других СУБД укажите его явно через ESCAPE.
Минимальные права в базе
Даже если инъекция найдётся, ущерб ограничат права пользователя БД:
- приложение подключается под отдельным пользователем, а не под суперпользователем;
- у него только
SELECT,INSERT,UPDATE,DELETEна нужные таблицы, безDROPиALTER; - миграции выполняет другой пользователь, только во время деплоя;
- для отчётов и аналитики — пользователь только на чтение;
- ошибки SQL пишутся в лог, а не показываются посетителю.
CREATE ROLE app_user LOGIN PASSWORD 'from-secrets-manager';
GRANT CONNECT ON DATABASE shop TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
Проверка через sqlmap
sqlmap — открытый инструмент, который автоматически ищет и эксплуатирует SQL-инъекции. Запускайте его только на своих системах, лучше на staging-копии: он отправляет много запросов и может изменить данные.
sqlmap -u "https://staging.example.com/product?id=10" --batch
sqlmap -u "https://staging.example.com/search" --data="q=test" --cookie="session=..." --batch
Помимо sqlmap полезны статический анализ (Semgrep, CodeQL) в CI и ревью каждого сырого запроса.
Чек-лист
- Все запросы параметризованы, без склейки строк.
- Сырые запросы ORM проверены вручную.
- Сортировка, имена таблиц и колонок — только из белого списка.
- Во вводе для LIKE экранированы
%и_. - Пользователь БД с минимальными правами.
- Ошибки SQL не видны посетителям.
- Регулярная проверка sqlmap и статическим анализом.