Что вообще такое SQL-инъекция?
Представьте, что у вас есть сайт с формой входа. Пользователь вводит логин и пароль, а ваш сервер проверяет их в базе данных. Всё просто, да? А теперь представьте, что злоумышленник вместо логина вводит специальный текст, который меняет логику вашего запроса к базе данных. Это и есть SQL-инъекция.
Звучит как магия? Сейчас разберёмся, как это работает, и - самое главное - как от этого защититься. Спойлер: это проще, чем кажется.
Как это работает (на пальцах)
SQL - это язык, на котором мы "разговариваем" с базой данных. Когда пользователь вводит логин на сайте, сервер отправляет в базу запрос примерно такого вида:
-- Это SQL-запрос. Он говорит базе данных:
-- "Найди мне пользователя с таким логином и паролем"
SELECT * FROM users WHERE username = 'ivan' AND password = '12345'
Если пользователь с таким логином и паролем существует - база вернёт его данные. Если нет - вернёт пустой результат. Логично? Абсолютно.
А теперь смотрите, что произойдёт, если злоумышленник вместо логина введёт вот это:
' OR 1=1 --
Наш запрос превратится в:
-- Вот что получилось:
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = 'неважно'
-- Разберём по частям:
-- username = '' - пустой логин (ну ладно)
-- OR 1=1 - ИЛИ 1 равно 1 (а это ВСЕГДА правда!)
-- -- - это комментарий, всё после него игнорируется
-- Результат: база возвращает ВСЕХ пользователей!
Условие 1=1 всегда истинно, а -- превращает проверку пароля в комментарий. Итог: злоумышленник получает доступ без пароля, обычно к первому аккаунту в базе (а это часто - администратор).
Как выглядит уязвимый код
Вот типичный пример кода на Node.js, который категорически нельзя использовать в реальных проектах:
// ОПАСНЫЙ КОД! Никогда так не делайте!
// Здесь пользовательский ввод напрямую вставляется в SQL-запрос
app.post('/api/login', async (req, res) => {
// req.body - это то, что пользователь отправил из формы
const username = req.body.username; // Например: "ivan"
const password = req.body.password; // Например: "12345"
// Вот тут ПРОБЛЕМА: мы просто подставляем текст от пользователя
// прямо в SQL-запрос, как будто это безопасные данные.
// Спойлер: это НЕ безопасные данные.
const query = `SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'`;
// Если username = "' OR 1=1 --", то запрос будет сломан
const [rows] = await db.execute(query);
if (rows.length > 0) {
res.json({ message: 'Добро пожаловать!' });
} else {
res.json({ message: 'Неверный логин или пароль' });
}
});
Проблема здесь в строке с обратными кавычками (шаблонная строка). Мы берём текст от пользователя и просто вклеиваем его в SQL. Это как дать незнакомцу дописать ваше письмо в банк - он может написать там что угодно.
Что может натворить злоумышленник
SQL-инъекция - это не просто "обойти логин". Вот что ещё можно сделать:
Украсть все данные
Допустим, на сайте есть поиск товаров. Злоумышленник может через поиск вытащить данные из совсем другой таблицы:
-- Нормальный запрос (ищем товары в категории "телефоны"):
SELECT name, price FROM products WHERE category = 'телефоны'
-- А вот что отправит злоумышленник в поле поиска:
-- ' UNION SELECT username, password FROM users --
--
-- Итоговый запрос:
SELECT name, price FROM products WHERE category = ''
UNION SELECT username, password FROM users --'
-- UNION объединяет результаты двух запросов
-- Вместо списка товаров вернутся логины и пароли!
-- Красиво? Нет. Страшно? Да.
Удалить всю базу
-- Злоумышленник вводит в любое поле:
-- '; DROP TABLE users; --
--
-- Итоговый запрос:
SELECT * FROM products WHERE name = ''; DROP TABLE users; --'
-- DROP TABLE users - удаляет таблицу с пользователями
-- Все данные пропали. Бэкап есть? Надеемся, что да.
Как защититься
Способ 1: Параметризованные запросы (самый надёжный)
Это главный и самый важный способ защиты. Идея простая: мы не вклеиваем данные пользователя в SQL напрямую, а передаём их отдельно. База данных сама разберётся, как их безопасно подставить.
// БЕЗОПАСНЫЙ КОД - используем параметризованные запросы
app.post('/api/login', async (req, res) => {
const username = req.body.username;
const password = req.body.password;
// Знак ? - это "placeholder" (заполнитель)
// Мы говорим базе: "вот тебе запрос, а вот отдельно - данные"
// База данных НИКОГДА не выполнит данные как SQL-код
const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ? AND password_hash = ?',
[username, password] // Эти значения подставятся вместо знаков ?
);
// Даже если username = "' OR 1=1 --", база будет искать
// пользователя с логином "' OR 1=1 --" (буквально такой текст)
// и, конечно, не найдёт. Инъекция не сработает!
if (rows.length > 0) {
res.json({ message: 'Добро пожаловать!' });
} else {
res.json({ message: 'Неверный логин или пароль' });
}
});
Ещё примеры параметризованных запросов для разных ситуаций:
// Добавление нового пользователя
// Три знака ? - три параметра
const [result] = await pool.execute(
'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
[username, email, hashedPassword]
);
// Обновление данных
const [result] = await pool.execute(
'UPDATE users SET email = ? WHERE id = ?',
[newEmail, userId]
);
// Поиск с фильтрами
const [products] = await pool.execute(
'SELECT * FROM products WHERE price BETWEEN ? AND ? AND category = ?',
[minPrice, maxPrice, category]
);
// Удаление (тоже с параметром!)
const [result] = await pool.execute(
'DELETE FROM sessions WHERE user_id = ?',
[userId]
);
Способ 2: ORM-библиотеки (ещё проще)
ORM (Object-Relational Mapping) - это библиотека, которая позволяет работать с базой данных, не написав ни строчки SQL. Она сама генерирует безопасные запросы. Вот примеры:
// Prisma - самый популярный ORM для Node.js
// Вместо SQL пишем обычный JavaScript/TypeScript
// Найти пользователя по email
// Prisma сама сделает безопасный SQL-запрос
const user = await prisma.user.findUnique({
where: {
email: req.body.email // Безопасно! Prisma сама экранирует
}
});
// Найти товары с фильтрами
const products = await prisma.product.findMany({
where: {
category: req.query.category, // Безопасно
price: {
gte: 100, // gte = greater than or equal (больше или равно)
lte: 5000 // lte = less than or equal (меньше или равно)
},
name: {
contains: req.query.search // Поиск по части названия
}
},
orderBy: { price: 'asc' }, // Сортировка по цене
take: 20 // Вернуть только 20 штук
});
// Создать нового пользователя
const newUser = await prisma.user.create({
data: {
username: req.body.username,
email: req.body.email,
passwordHash: hashedPassword
}
});
Способ 3: Проверка входных данных (дополнительная страховка)
Даже если вы используете параметризованные запросы, полезно проверять что пользователь отправляет. Это как ремень безопасности плюс подушка безопасности - лишним не будет:
// Используем библиотеку Zod для проверки данных
import { z } from 'zod';
// Описываем правила: какие данные мы ожидаем
const loginSchema = z.object({
// username должен быть строкой от 3 до 50 символов
// и содержать только буквы, цифры и подчёркивание
username: z.string()
.min(3, 'Логин должен быть минимум 3 символа')
.max(50, 'Логин слишком длинный')
.regex(/^[a-zA-Z0-9_]+$/, 'Только буквы, цифры и _'),
// password - строка от 8 до 128 символов
password: z.string()
.min(8, 'Пароль должен быть минимум 8 символов')
.max(128, 'Пароль слишком длинный')
});
app.post('/api/login', async (req, res) => {
// Проверяем данные перед использованием
const result = loginSchema.safeParse(req.body);
// Если данные не прошли проверку - возвращаем ошибку
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten().fieldErrors
});
// Пример ответа: { errors: { username: ["Только буквы, цифры и _"] } }
}
// Теперь данные точно корректные
const { username, password } = result.data;
// И используем параметризованный запрос (двойная защита!)
const [rows] = await pool.execute(
'SELECT * FROM users WHERE username = ?',
[username]
);
// ... дальше проверяем пароль
});
Дополнительная защита на уровне базы данных
Даже если злоумышленник каким-то чудом пробьёт все защиты, можно ограничить ущерб. Для этого создайте отдельного пользователя базы данных с минимальными правами:
-- Создаём пользователя, который может только ЧИТАТЬ данные
-- (для публичной части сайта, где данные только показываются)
CREATE USER 'app_reader'@'localhost' IDENTIFIED BY 'надёжный_пароль';
-- Даём ему право только на чтение, и только в конкретных таблицах
GRANT SELECT ON mydb.products TO 'app_reader'@'localhost';
GRANT SELECT ON mydb.categories TO 'app_reader'@'localhost';
-- Этот пользователь НЕ МОЖЕТ удалить или изменить данные!
-- Для админ-панели - отдельный пользователь с правами на запись
CREATE USER 'app_admin'@'localhost' IDENTIFIED BY 'другой_надёжный_пароль';
GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_admin'@'localhost';
-- Но даже он НЕ МОЖЕТ удалять таблицы (нет прав на DROP)
Как проверить свой сайт на уязвимости
Есть специальные инструменты, которые автоматически проверяют сайт на SQL-инъекции:
# sqlmap - бесплатный инструмент для тестирования
# Установка: pip install sqlmap
# Проверить GET-запрос (где параметры в URL)
sqlmap -u "http://localhost:3000/api/products?category=test" --batch
# Проверить POST-запрос (где данные в форме)
sqlmap -u "http://localhost:3000/api/login"
--data="username=admin&password=test"
--batch
# --batch означает "не задавай вопросов, делай всё автоматически"
Важно: используйте такие инструменты только на своих сайтах или с разрешения владельца. Тестирование чужих сайтов без разрешения - это незаконно.
Памятка (повесьте над монитором)
- Всегда используйте параметризованные запросы (знаки
?) или ORM - Никогда не вклеивайте данные от пользователя прямо в SQL-строку
- Проверяйте входные данные с помощью
zodилиjoi - Создавайте отдельных пользователей БД с минимальными правами
- Не показывайте пользователям технические ошибки базы данных
- Регулярно обновляйте библиотеки и драйверы
- Тестируйте свой сайт инструментами вроде sqlmap
Итого
SQL-инъекция - это когда злоумышленник подсовывает вредоносный код через обычное поле ввода на сайте. Защита элементарная: не вклеивайте данные от пользователя в SQL напрямую. Используйте параметризованные запросы (знаки ?) или ORM - и проблема решена раз и навсегда.
Это как закрывать дверь на замок - не гарантирует 100% безопасности, но без этого вы просто приглашаете воров в гости. И да, бэкапы базы данных тоже делайте. На всякий случай. Потому что "всякий случай" имеет привычку случаться в пятницу вечером, когда вы уже открыли пиво.