Перейти к содержанию
Innovialabs

Как защититься от SQL-инъекций: параметризованные запросы и ORM

Практика4 мин

Коротко

От 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() с данными
SQLAlchemytext("... :email") с параметрамиf-строка внутри text()
LaravelwhereRaw('email = ?', [$email])DB::raw("... $email")
Sequelizequery(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 и ревью каждого сырого запроса.

Чек-лист

  1. Все запросы параметризованы, без склейки строк.
  2. Сырые запросы ORM проверены вручную.
  3. Сортировка, имена таблиц и колонок — только из белого списка.
  4. Во вводе для LIKE экранированы % и _.
  5. Пользователь БД с минимальными правами.
  6. Ошибки SQL не видны посетителям.
  7. Регулярная проверка sqlmap и статическим анализом.

Частые вопросы

Только если внутри них нет динамического SQL со склейкой строк. Процедура, которая собирает запрос из параметров, так же уязвима.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.