$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогSQL-инъекции и безопасные запросы: практики, которые реально защищают

SQL-инъекции и безопасные запросы: практики, которые реально защищают

$ sudo teach IT
·10 августа 2026 г.·11 мин·30
SQL-инъекции и безопасные запросы: практики, которые реально защищают

Соберём чек-лист защиты от SQL-инъекций и ошибок обработки данных: параметризация, валидация идентификаторов, работа со списками (IN), безопасная сортировка/пагинация и как проверять, что ORM/клиент используют биндинг. Добавим примеры “как было” и “как пр

Содержание
1) Понимание угрозы: где рождаются SQL-инъекцииЧто считается опаснымЧто считается безопасным2) Параметризация: базовый барьер, который действительно работаетКак было (опасно)Как правильно (параметры)Нюанс: «параметризация» в кавычках и подстановка идентификаторов3) Валидация идентификаторов: типы, форматы и режимы отказаИдентификаторы — строго по типуОшибки валидации — по контракту, не по логике SQL4) Работа с IN (...): списки без сборки SQL-строкиКак было (опасная сборка)Как правильно: параметризовать элементыПограничный случай: пустой списокАльтернатива: массивы и = ANY(...) (PostgreSQL)5) Безопасная сортировка: ORDER BY не параметризуется напрямуюКак было (опасно)Как правильно: белый список колонокЕщё один нюанс: таблицы/алиасы6) Пагинация: LIMIT/OFFSET тоже требуют валидацииКак былоКак правильноЧем заменить OFFSET для больших данных7) Проверка того, что ORM/клиент действительно делает bindЧто проверятьМини-паттерн теста (идея)Важно: сообщения об ошибках8) Ошибки обработки данных: не только инъекция, но и утечки/невалидная логикаНормализация и сравнение строкNULL и пустые значенияОшибки/переполнения9) Чек-лист: практики, которые реально защищаютA. ПараметризацияB. Валидация входных значенийC. Динамические фрагменты SQLD. Списки и массивыE. ORM/клиенты и биндинг10) «Как было / как правильно»: компактные примеры для ревью10.1 Фильтр по id10.2 Список IN10.3 СортировкаВывод: безопасность — это система, а не разовый фикс

SQL-инъекция — одна из тех уязвимостей, которые «вроде бы уже все знают», но на практике продолжают встречаться почти в каждой экосистеме: от простых CRUD-приложений до корпоративных систем с ORM и сложными отчётами. Причина не в том, что разработчики «плохо стараются», а в том, что инъекция — это не отдельный баг в одном месте. Это следствие целого ряда архитектурных решений: где и как формируется SQL, как обрабатываются идентификаторы и фильтры, как устроены сортировка и пагинация, насколько строго валидируются входные данные и какие механизмы биндинга реально используются.

Ниже — практический чек-лист и разбор типичных ошибок. Мы разберём «как было» (классические сценарии, которые приводят к инъекциям) и «как правильно» (подходы, которые реально снижают риск). Отдельное внимание уделим случаям, которые часто обходят стороной: IN (...), динамический ORDER BY, пагинация, а также проверка того, что ORM/клиент реально применяют параметризацию.


1) Понимание угрозы: где рождаются SQL-инъекции

SQL-инъекция возникает, когда пользовательский ввод попадает в структуру SQL как часть синтаксиса, а не как данные. Данные и синтаксис должны разделяться на уровне API драйвера БД: SQL-шаблон фиксирован, а значения передаются через параметры (bind/prepare).

Что считается опасным

  • Склейка строк: "... WHERE id = " + input
  • Использование форматирования, которое подставляет пользовательские значения в SQL-часть
  • «Экранирование вручную» (например, replace("'", "''")) как единственный механизм защиты
  • Динамический ORDER BY/GROUP BY/LIMIT на основе ввода без строгого белого списка
  • Конструкция вроде IN (" + join(inputs) + ")" — часто превращается в инъекцию при некорректной обработке элементов

Что считается безопасным

  • Параметризация запросов (prepared statements / bind parameters)
  • Чёткая валидация типов (например, идентификатор — строго число/UUID)
  • Белые списки для «динамических» фрагментов SQL, которые нельзя параметризовать напрямую (чаще всего это колонки для сортировки)
  • Системная проверка фактического поведения ORM/клиента (а не уверенность «у нас всё на параметрах»)

2) Параметризация: базовый барьер, который действительно работает

Параметризация — это не стиль, а контракт с драйвером: вы передаёте SQL как шаблон, а значения — отдельно. Драйвер сам формирует протокол передачи и не даёт данным стать частью синтаксиса.

Как было (опасно)

Пример на псевдокоде (склейка строк):

code
-- Вход: input = "1 OR 1=1"
SELECT * FROM users WHERE id = 1 OR 1=1;

Если в приложении было что-то вроде:

code
// НЕ ДЕЛАТЬ
const sql = "SELECT * FROM users WHERE id = " + req.query.id;

Инъекция может изменить логику запроса: обойти фильтры, вывести чужие записи, иногда — выполнить побочные действия в зависимости от БД/драйвера.

Как правильно (параметры)

code
SELECT * FROM users WHERE id = ?;

И в коде:

code
const sql = "SELECT * FROM users WHERE id = ?";
const params = [id];
await db.query(sql, params);

Даже если id будет "1 OR 1=1", драйвер передаст это как строку/значение, а не как SQL-фрагмент.

Нюанс: «параметризация» в кавычках и подстановка идентификаторов

Важно понимать: параметризация работает для значений. Но она не спасает, если вы подставляете в запрос структуру SQL как строку.

Например, это безопасно:

  • WHERE status = :status (параметр)

А это потенциально опасно (даже если кажется, что вы «всё экранировали»):

  • ORDER BY ${sort} (потому что sort становится частью SQL)

Параметры не заменяют белые списки там, где меняется синтаксис.


3) Валидация идентификаторов: типы, форматы и режимы отказа

Параметризация защищает от инъекций, но валидация входных данных нужна для корректности и дополнительной безопасности: чтобы запросы не превращались в «мягкую» логику отказа или не создавали неожиданных веток.

Идентификаторы — строго по типу

  • id числовой — проверяйте, что это число (и часто — что оно в разумном диапазоне)
  • uuid — проверяйте формат UUID
  • составные идентификаторы — валидируйте каждый компонент

Как было (через неявные приведения)

code
// req.query.id = "abc"
const sql = "SELECT * FROM users WHERE id = ?";
await db.query(sql, [req.query.id]);

На некоторых драйверах и в некоторых режимах БД это может:

  • привести к ошибкам (и возможной утечке деталей через сообщения),
  • вызвать неожиданное приведение типов,
  • либо просто дать некорректные результаты.

Как правильно

code
function parseIntId(value) {
  const n = Number(value);
  if (!Number.isInteger(n) || n < 1) {
    throw new Error("Invalid id");
  }
  return n;
}

const id = parseIntId(req.query.id);

const sql = "SELECT * FROM users WHERE id = ?";
await db.query(sql, [id]);

Ошибки валидации — по контракту, не по логике SQL

Ошибку нужно возвращать приложением (например, 400 Bad Request), а не пропускать через БД и «смотреть, что получится». Это сокращает риск утечек и уменьшает шанс на побочные эффекты.


4) Работа с IN (...): списки без сборки SQL-строки

Списки — один из самых частых источников SQL-инъекций, потому что разработчики пытаются «склеить» IN вручную.

Как было (опасная сборка)

code
// req.query.ids = "1) OR 1=1 --"
const ids = req.query.ids.split(",");
const sql = "SELECT * FROM users WHERE id IN (" + ids.join(",") + ")";
await db.query(sql);

Если в одном из элементов окажется подстрока с SQL, она станет частью синтаксиса.

Как правильно: параметризовать элементы

Самый надёжный подход — формировать плейсхолдеры под количество элементов и передавать параметры отдельно.

PostgreSQL / MySQL (в общем виде)

code
const ids = req.query.ids.split(",").map(parseIntId); // строгая валидация
const placeholders = ids.map(() => "?").join(",");   // ?,?,?,...
const sql = `SELECT * FROM users WHERE id IN (${placeholders})`;

await db.query(sql, ids);

SQL шаблон остаётся неизменным по структуре, а значения — биндингуются.

Пограничный случай: пустой список

WHERE id IN () синтаксически неверно. Это важно обработать заранее.

Варианты:

  • верните пустой результат без запроса,
  • или используйте безопасную логическую заглушку, например WHERE 1=0.

Пример:

code
if (ids.length === 0) {
  return []; // или await db.query("SELECT ... WHERE 1=0");
}

Альтернатива: массивы и = ANY(...) (PostgreSQL)

В PostgreSQL можно передавать массив как параметр и использовать = ANY($1):

code
SELECT * FROM users WHERE id = ANY($1);

В коде:

code
const ids = req.query.ids.split(",").map(parseIntId);
await db.query(sql, [ids]);

Это уменьшает количество плейсхолдеров, упрощает запрос и часто удобнее для больших списков.


5) Безопасная сортировка: ORDER BY не параметризуется напрямую

Сортировка — «динамический» фрагмент SQL. Драйверы и ORM обычно позволяют параметризовать значения, но не идентификаторы (имена колонок) и не синтаксические части. Поэтому риск возникает именно там: вы можете сделать ORDER BY ${sort} и получить инъекцию через имя колонки.

Как было (опасно)

code
const sort = req.query.sort; // "id; DROP TABLE users; --"
const sql = `SELECT * FROM users ORDER BY ${sort} LIMIT ? OFFSET ?`;
await db.query(sql, [limit, offset]);

Даже если limit/offset параметризованы, колонка для ORDER BY — часть SQL-строки.

Как правильно: белый список колонок

  • Разрешите только заранее известные колонки
  • Для направления сортировки (asc/desc) тоже используйте белый список
code
const sortBy = req.query.sortBy;
const sortDir = req.query.sortDir;

const sortColumns = {
  id: "id",
  name: "name",
  createdAt: "created_at"
};

const column = sortColumns[sortBy];
if (!column) throw new Error("Invalid sort column");

const direction = (sortDir === "asc" || sortDir === "desc") ? sortDir : "desc";

const sql = `SELECT * FROM users ORDER BY ${column} ${direction} LIMIT ? OFFSET ?`;
await db.query(sql, [limit, offset]);

Обратите внимание: мы подставляем только значения из белого списка. Пользовательский ввод напрямую в SQL не попадает.

Ещё один нюанс: таблицы/алиасы

Если у вас сложный запрос с JOIN, тоже держите белый список: users.id, profiles.name и т.п. Это снижает риск ошибок из-за неоднозначных имён и случайной подстановки.


6) Пагинация: LIMIT/OFFSET тоже требуют валидации

LIMIT и OFFSET часто параметризуются, но ключевой момент — их всё равно нужно валидировать как числа и контролировать диапазоны. Иначе можно получить:

  • чрезмерную нагрузку (LIMIT 1000000)
  • ошибки драйвера
  • поведение, которое усложнит защиту на уровне бизнес-логики

Как было

code
const limit = req.query.limit; // "999999999999"
const offset = req.query.offset; // "-10"
const sql = "SELECT * FROM users LIMIT " + limit + " OFFSET " + offset;
await db.query(sql);

Снова склейка + отсутствие проверок.

Как правильно

code
function parseNonNegativeInt(value, {max = 100} = {}) {
  const n = Number(value);
  if (!Number.isInteger(n) || n < 0 || n > max) {
    throw new Error("Invalid pagination parameter");
  }
  return n;
}

const limit = parseNonNegativeInt(req.query.limit, { max: 100 });
const offset = parseNonNegativeInt(req.query.offset, { max: 10000 });

const sql = "SELECT * FROM users LIMIT ? OFFSET ?";
await db.query(sql, [limit, offset]);

Чем заменить OFFSET для больших данных

Для больших таблиц OFFSET ухудшает производительность. Безопасность — не единственная цель, но хороший практический уровень включает:

  • keyset pagination (пагинация по последнему значению сортировки),
  • индексы под поля сортировки.

Это уже больше про производительность, но косвенно снижает риск DoS через тяжёлые запросы.


7) Проверка того, что ORM/клиент действительно делает bind

Одна из самых неприятных ситуаций: разработчик уверен, что «ORM параметризует всегда», а на деле в конкретном фрагменте используется сырая вставка (raw, literal, order строкой и т.д.).

Что проверять

  1. Логи SQL и параметры
    Включите валидацию на уровне среды: логируйте запрос и отдельно — параметры.
  2. Фактический запрос к БД
    Смотрите, где значения появляются в SQL, а где — остаются плейсхолдерами.
  3. Использование raw/literal
    Если ORM предоставляет способы «вставить кусок SQL», нужно считать это зоной повышенного риска.
  4. Тесты на инъекцию
    Напишите интеграционный тест: передайте значения вроде ' OR 1=1 -- и убедитесь, что запрос отрабатывает безопасно (или возвращает 400 при валидации), но не меняет логику.

Мини-паттерн теста (идея)

  • Если поле id проходит через биндинг, запрос не «взломает» фильтр
  • Если поле сортировки — белый список, инъекция не пройдёт в синтаксис

В реальности конкретный код зависит от стека, но логика проверки одинаковая: вы тестируете не «ожидание разработчика», а наблюдаемый эффект.

Важно: сообщения об ошибках

Если БД возвращает ошибки «syntax error at or near…», часто это значит, что пользовательский ввод попал в SQL. Плохая новость: это помогает атакующему подбирать инъекцию. Хорошая новость: это хороший сигнал для разработчика — значит, нужно исправлять место формирования запроса.


8) Ошибки обработки данных: не только инъекция, но и утечки/невалидная логика

SQL-инъекция — яркий пример, но безопасность запросов включает и более «тихие» проблемы:

Нормализация и сравнение строк

  • Не полагайтесь на то, что trim сделает «всё правильно»
  • Используйте валидацию по типу: если ожидается email/телефон — валидируйте формат

NULL и пустые значения

Разные СУБД и ORM могут по-разному трактовать NULL в фильтрах. Это не напрямую SQL-инъекция, но может привести к логическим обходам условий доступа.

Ошибки/переполнения

Если входной параметр приводит к исключениям в коде SQL-уровня, проверьте:

  • нет ли утечек в стек трейс,
  • нет ли режима, когда запрос собирается частично.

9) Чек-лист: практики, которые реально защищают

Ниже — структурированный список, который удобно перенести в внутренний стандарт разработки или чек при ревью PR.

A. Параметризация

  • Все пользовательские значения передаются через bind/параметры, а не через конкатенацию
  • В запросах нет склеек вида " ... " + userInput + " ..."
  • Для IN (...) используются плейсхолдеры и массив параметров, а не сборка строки

B. Валидация входных значений

  • Идентификаторы валидируются по типу (int/UUID) и по диапазону
  • Пагинация ограничена: limit имеет верхний предел, offset не отрицательный
  • Ошибки валидации возвращаются приложением (обычно 400), а не «как получилось»

C. Динамические фрагменты SQL

  • Для сортировки используется белый список колонок
  • Для направления сортировки (asc/desc) используется строгий белый список
  • Запрещены вставки ORDER BY ${user} без маппинга через whitelist

D. Списки и массивы

  • Пустые списки обрабатываются отдельно (без IN ())
  • Для PostgreSQL при необходимости используется = ANY($1) (массив как параметр)

E. ORM/клиенты и биндинг

  • Логи SQL показывают плейсхолдеры, а параметры отдельно
  • Использование raw/literal — только с белыми списками и/или строгой санитизацией
  • Добавлены интеграционные тесты на инъекционные входы

10) «Как было / как правильно»: компактные примеры для ревью

10.1 Фильтр по id

Как было (опасно):

code
const sql = "SELECT * FROM users WHERE id = " + req.query.id;

Как правильно:

code
const id = parseIntId(req.query.id);
const sql = "SELECT * FROM users WHERE id = ?";
await db.query(sql, [id]);

10.2 Список IN

Как было (опасно):

code
const sql = "SELECT * FROM users WHERE id IN (" + ids.join(",") + ")";

Как правильно:

code
if (ids.length === 0) return [];
const placeholders = ids.map(() => "?").join(",");
const sql = `SELECT * FROM users WHERE id IN (${placeholders})`;
await db.query(sql, ids);

10.3 Сортировка

Как было (опасно):

code
const sql = `SELECT * FROM users ORDER BY ${req.query.sortBy} ${req.query.dir}`;

Как правильно:

code
const sortColumns = { id: "id", name: "name", createdAt: "created_at" };
const column = sortColumns[req.query.sortBy];
if (!column) throw new Error("Invalid sort column");

const direction = req.query.dir === "asc" ? "asc" : "desc";
const sql = `SELECT * FROM users ORDER BY ${column} ${direction} LIMIT ? OFFSET ?`;
await db.query(sql, [limit, offset]);

Вывод: безопасность — это система, а не разовый фикс

SQL-инъекции редко появляются «из ниоткуда». Обычно это цепочка: где-то удобство взяло верх над контрактом параметризации, где-то сортировка/фильтры стали динамическими без белого списка, где-то списки IN (...) собирались вручную. Поэтому защита должна быть системной: параметризация для значений, строгая валидация идентификаторов и пагинации, белые списки для синтаксических частей вроде ORDER BY, корректная обработка списков и проверка фактического поведения ORM/клиента.

Если вы только углубляетесь в тему, полезно систематизировать базу: как именно формируются запросы, какие риски существуют и почему «экранирование» не заменяет биндинг. В этом смысле курс «SQL – для начинающих!» может стать аккуратным способом закрыть пробелы — не вместо практик выше, а как фундамент, на который затем ложатся безопасные шаблоны запросов.

В реальных проектах лучший результат дают два действия: (1) стандартизировать подход (чек-лист и шаблоны), (2) подтверждать безопасность наблюдаемыми фактами — логами SQL, интеграционными тестами и ревью мест, где используются динамические SQL-фрагменты или raw/literal.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

SQL – для начинающих!

Курс по теме

SQL – для начинающих!

С нуля до уверенных запросов на SQL. Никакой теории без дела — только живые запросы, настоящие данные и готовность к работе с любой базой. Хватит гуглить «как написать JOIN» — разберись один раз и забудь.

Бесплатно
Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Пишем миграции баз данных без сюрпризов: безопасные схемы изменения таблиц
пишем

Пишем миграции баз данных без сюрпризов: безопасные схемы изменения таблиц

Научимcя менять структуру БД так, чтобы минимизировать блокировки: фазовые миграции, бэкапы, совместимые изменения и откаты.

23 июля 2026 г.
520
SQL JOIN за 10 минут: наглядный гид с реальными примерами
join

SQL JOIN за 10 минут: наглядный гид с реальными примерами

Объясняем INNER, LEFT, RIGHT и FULL JOIN на живых таблицах — без воды и абстрактных схем, только запросы и результаты.

16 июля 2026 г.
510
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1101
Какой первый проект выбрать новичку, чтобы не бросить обучение
первый

Какой первый проект выбрать новичку, чтобы не бросить обучение

Подберём 5–7 идей под уровень “с нуля”, объясним, что делать по шагам и как довести проект до результата без перегруза. В конце — как оформить мини-портфолио и что показать, даже если проект маленький.

23 сентября 2026 г.
200
FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь
fastapi

FastAPI и фоновые задачи: когда лучше использовать BackgroundTasks, а когда очередь

Поймём различия между синхронной обработкой, BackgroundTasks и внешними очередями. Разберём idempotency, ретраи и мониторинг фоновых процессов.

22 июля 2026 г.
770
Как выучить программирование самому и бесплатно: с чего начать и в каком порядке
выучить

Как выучить программирование самому и бесплатно: с чего начать и в каком порядке

Разберём понятный маршрут для новичка: как выбрать первый язык, что учить в первую очередь, какие мини-проекты делать и как не забросить занятия. Дам бесплатные ориентиры и план, чтобы знания складывались в портфолио.

21 сентября 2026 г.
300

Комментарии

Пока нет комментариев

Содержание

1) Понимание угрозы: где рождаются SQL-инъекцииЧто считается опаснымЧто считается безопасным2) Параметризация: базовый барьер, который действительно работаетКак было (опасно)Как правильно (параметры)Нюанс: «параметризация» в кавычках и подстановка идентификаторов3) Валидация идентификаторов: типы, форматы и режимы отказаИдентификаторы — строго по типуОшибки валидации — по контракту, не по логике SQL4) Работа с IN (...): списки без сборки SQL-строкиКак было (опасная сборка)Как правильно: параметризовать элементыПограничный случай: пустой списокАльтернатива: массивы и = ANY(...) (PostgreSQL)5) Безопасная сортировка: ORDER BY не параметризуется напрямуюКак было (опасно)Как правильно: белый список колонокЕщё один нюанс: таблицы/алиасы6) Пагинация: LIMIT/OFFSET тоже требуют валидацииКак былоКак правильноЧем заменить OFFSET для больших данных7) Проверка того, что ORM/клиент действительно делает bindЧто проверятьМини-паттерн теста (идея)Важно: сообщения об ошибках8) Ошибки обработки данных: не только инъекция, но и утечки/невалидная логикаНормализация и сравнение строкNULL и пустые значенияОшибки/переполнения9) Чек-лист: практики, которые реально защищаютA. ПараметризацияB. Валидация входных значенийC. Динамические фрагменты SQLD. Списки и массивыE. ORM/клиенты и биндинг10) «Как было / как правильно»: компактные примеры для ревью10.1 Фильтр по id10.2 Список IN10.3 СортировкаВывод: безопасность — это система, а не разовый фикс