SQL-инъекция — это одна из тех тем, которые лучше понять до того, как ты выкатил первую форму логина, поиск по сайту или фильтр заказов
Сама идея простая: приложение строит SQL-запрос из строки, в которую попали пользовательские данные. Если эти данные склеиваются с SQL-кодом напрямую, пользователь может неожиданно повлиять не только на значение, но и на смысл запроса
Этот урок не про взлом. Он про защиту: как увидеть опасный паттерн в своем коде и заменить его на параметризованный запрос
- Где возникает проблема
- Правильный вариант: параметры вместо склейки
- Логин: плохой и нормальный пример
- Почему экранирование не лучший первый ответ
- Где параметры не помогают напрямую
- Мини-чеклист для своего проекта
- Принцип на уровне базы: минимальные права
- Что делать в учебном проекте
- Частые ошибки
- "У меня админка, туда никто не зайдет"
- "ORM сам все защитит"
- "Я проверил email регуляркой"
- "Я экранирую кавычки"
- Безопасное мини-задание
- Ответы на эти вопросы могут быть для вас полезными
- Что такое SQL-инъекция?
- Как защититься от SQL-инъекции?
- Достаточно ли фильтровать кавычки?
- ORM защищает автоматически?
- Нужно ли новичку изучать SQL-инъекции?
- Что почитать дальше по SQL
Где возникает проблема
Представим форму поиска по email. Пользователь вводит email, приложение ищет пользователя в базе
Опасный подход выглядит так:
const sql = "SELECT id, email FROM users WHERE email = '" + email + "'";
const rows = await db.query(sql);
Пока пользователь вводит обычный email, все кажется рабочим:
anna@example.com
Запрос становится таким:
SELECT id, email FROM users WHERE email = 'anna@example.com'
Проблема в том, что приложение не отделило SQL-код от данных. Оно просто склеило строку. Если в email попадет специальный SQL-фрагмент, база может воспринять часть пользовательского ввода как продолжение запроса
OWASP в своих материалах по SQL Injection Prevention прямо рекомендует перестать писать динамические запросы через конкатенацию строк и использовать prepared statements с параметрами. Смысл защиты: сначала описывается структура SQL, потом отдельно передаются значения
Правильный вариант: параметры вместо склейки
Пример для Node.js в стиле популярных SQL-клиентов:
const sql = "SELECT id, email FROM users WHERE email = ?";
const [rows] = await db.execute(sql, [email]);
Здесь ? — место для значения. SQL-код и данные передаются отдельно. База понимает: это структура запроса, а это значение, которое нужно сравнить с колонкой email
Даже если пользователь введет странную строку со спецсимволами, она останется значением. Она не превратится в новый SQL-код
Логин: плохой и нормальный пример
Плохой пример:
const sql =
"SELECT id FROM users WHERE email = '" + email +
"' AND password_hash = '" + passwordHash + "'";
const rows = await db.query(sql);
Здесь и email, и hash пароля склеиваются с SQL-запросом. Это опасный шаблон
Нормальный пример:
const sql = `
SELECT id
FROM users
WHERE email = ?
AND password_hash = ?
`;
const [rows] = await db.execute(sql, [email, passwordHash]);
Важно: это пример именно про SQL-защиту. В реальном приложении пароль нельзя хранить обычным текстом. Его хранят как результат безопасного password hashing, а сравнение делают по правилам выбранной библиотеки
Почему экранирование не лучший первый ответ
Иногда новичок думает: "Я просто заменю кавычки, и все будет нормально"
Ручное экранирование быстро превращается в хрупкую самодельную систему:
- разные базы по-разному работают со строками;
- можно забыть обработать одно поле;
- можно ошибиться с кодировкой;
- можно начать динамически подставлять имена колонок или сортировку;
- можно получить ложное чувство безопасности.
OWASP описывает экранирование пользовательского ввода как нежелательный основной способ защиты. Гораздо надежнее использовать параметризованные запросы, а в местах, где параметры неприменимы, использовать allow-list
Где параметры не помогают напрямую
Параметры отлично подходят для значений:
WHERE email = ?
WHERE id = ?
WHERE created_at >= ?
Но ими обычно нельзя безопасно подставлять имена таблиц, имена колонок или направление сортировки:
ORDER BY ?
Для таких случаев используют белый список допустимых значений
Например, пользователь может выбрать сортировку:
const allowedSort = {
date: "created_at",
amount: "amount",
email: "email"
};
const sortColumn = allowedSort[sortBy] ?? "created_at";
const sql = `SELECT id, email, amount FROM orders ORDER BY ${sortColumn} DESC`;
Мы не вставляем пользовательскую строку напрямую. Мы выбираем значение из заранее заданной карты
Для направления сортировки:
const direction = sortDirection === "asc" ? "ASC" : "DESC";
const sql = `SELECT id, email, amount FROM orders ORDER BY ${sortColumn} ${direction}`;
Здесь пользователь не управляет SQL-фрагментом напрямую. Он выбирает из двух безопасных вариантов
Мини-чеклист для своего проекта
Проверь код проекта поиском по таким признакам:
- SQL-запрос собирается через
+; - SQL-запрос собирается через template string с пользовательскими данными;
WHEREсодержит напрямую значение из формы;ORDER BYстроится из query-параметра без белого списка;- в коде есть комментарий "потом защитить";
- используется один database user с избыточными правами.
Опасный пример:
const sql = `SELECT * FROM orders WHERE status = '${status}'`;
Лучше:
const sql = "SELECT * FROM orders WHERE status = ?";
const [rows] = await db.execute(sql, [status]);
Для статуса можно добавить allow-list:
const allowedStatuses = new Set(["paid", "pending", "cancelled"]);
if (!allowedStatuses.has(status)) {
throw new Error("Unexpected status");
}
const sql = "SELECT * FROM orders WHERE status = ?";
const [rows] = await db.execute(sql, [status]);
Принцип на уровне базы: минимальные права
Даже если код защищен, аккаунт приложения в базе не должен быть всемогущим
Если сайту нужно только читать каталог товаров, ему не нужны права на удаление таблиц. Если форме логина нужно читать пользователей, ей не нужны права администратора базы
OWASP отдельно выделяет least privilege: выдавать учетным записям только те права, которые им действительно нужны. Это не заменяет параметризованные запросы, но уменьшает ущерб, если где-то все же случится ошибка
Что делать в учебном проекте
Если ты делаешь маленький сайт, CRM или личный кабинет, я бы ввел правило сразу:
- Все значения из пользователя идут в параметры.
- Имена колонок и направление сортировки идут только через белый список.
- Пароли не хранятся открытым текстом.
- Ошибки базы не показываются пользователю целиком.
- У database user нет лишних прав.
- Для опасных изменений есть отдельные проверки и логирование.
Такой подход не делает приложение идеальным, но убирает самый грубый класс ошибок
Частые ошибки
"У меня админка, туда никто не зайдет"
Админки тоже взламывают, пароли утекают, внутренние формы становятся внешними после очередной доработки. Если данные приходят от человека или внешней системы, относись к ним как к недоверенным
"ORM сам все защитит"
ORM часто помогает, но не спасает, если ты вставляешь raw SQL со склейкой строк. Любой raw-запрос нужно проверять отдельно
"Я проверил email регуляркой"
Валидация полезна, но не заменяет параметры. Email можно проверять как email, а в SQL все равно передавать как параметр
"Я экранирую кавычки"
Ручное экранирование легко сделать неполным. Для значений используй параметризованные запросы
Безопасное мини-задание
Возьми любой свой учебный код, где есть SQL-запрос, и найди места, где пользовательские данные попадают в строку запроса
Плохой шаблон:
const sql = "SELECT * FROM users WHERE email = '" + email + "'";
Переделай в параметризованный:
const sql = "SELECT * FROM users WHERE email = ?";
const [rows] = await db.execute(sql, [email]);
Затем отдельно проверь сортировку и фильтры. Если там нельзя использовать параметры, сделай allow-list
Ответы на эти вопросы могут быть для вас полезными
Что такое SQL-инъекция?
Это уязвимость, при которой пользовательский ввод влияет на структуру SQL-запроса, а не остается обычным значением
Как защититься от SQL-инъекции?
Основной практический шаг — использовать параметризованные запросы или prepared statements. Для имен колонок, таблиц и сортировки нужен белый список допустимых значений
Достаточно ли фильтровать кавычки?
Нет. Ручная фильтрация и экранирование хрупкие. Их нельзя считать главным уровнем защиты
ORM защищает автоматически?
Часто помогает, если ты используешь обычные методы ORM. Но raw SQL со склейкой строк остается опасным даже внутри проекта с ORM
Нужно ли новичку изучать SQL-инъекции?
Да, на оборонительном уровне. Достаточно понять опасность конкатенации строк, привыкнуть к параметрам и не выдавать приложению лишние права в базе
Что почитать дальше по SQL
Если вы собираете тему по шагам, рядом лучше открыть:
- INSERT, UPDATE, DELETE без потери данных — аккуратно менять данные и проверять запрос перед выполнением.
- MySQL скачать и установить для первого SQL проекта — проверить параметризованные запросы в учебном проекте.
- EXISTS в SQL на простых примерах — увидеть, как сложные условия остаются безопасными с параметрами.
- SQL с нуля: первый запрос, фильтр и сортировка — вернуться к базовой структуре SQL-запроса.



