SQL Injection
SQL Injection — атака, при которой злоумышленник внедряет SQL-код через пользовательский ввод, получая доступ к базе данных.
Зачем нужно
SQL Injection позволяет: прочитать всю БД (пароли, платёжные данные), изменить или удалить данные, обойти аутентификацию, выполнить команды на сервере. Это одна из самых опасных и, к сожалению, распространённых уязвимостей.
Где используется
Любое приложение, которое строит SQL-запросы из пользовательского ввода: формы логина, поиск, фильтры, API с параметрами. Node.js + MySQL/PostgreSQL, PHP, Python, Java.
Предпосылки
Базовые знания SQL (SELECT, INSERT, WHERE), XSS, CSRF
Как работает SQL Injection
Уязвимый код
// ОПАСНО: конкатенация пользовательского ввода в SQL
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'`;
const user = await db.query(query);
if (user.length > 0) {
res.json({ success: true });
}
});
Атака
Ввод пользователя:
username: admin' --
password: что угодно
Итоговый SQL:
SELECT * FROM users
WHERE username = 'admin' --'
AND password = 'что угодно'
-- это комментарий SQL, остаток запроса игнорируется!
Результат: вход как admin БЕЗ пароля
Другие примеры атак
-- 1. Обход аутентификации
username: ' OR 1=1 --
→ SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = '...'
→ Возвращает ВСЕХ пользователей
-- 2. Извлечение данных (UNION)
search: ' UNION SELECT username, password FROM users --
→ SELECT name, price FROM products WHERE name = ''
UNION SELECT username, password FROM users --'
→ Выдаёт логины и пароли
-- 3. Удаление таблицы
input: '; DROP TABLE users; --
→ SELECT * FROM users WHERE id = ''; DROP TABLE users; --'
→ Удаляет таблицу users!
-- 4. Чтение файлов (MySQL)
input: ' UNION SELECT LOAD_FILE('/etc/passwd'), 2 --
Защита от SQL Injection
1. Параметризованные запросы (Prepared Statements)
Главный и обязательный метод защиты. Параметры передаются отдельно от SQL.
// Node.js + mysql2
const mysql = require('mysql2/promise');
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// БЕЗОПАСНО: параметры через ?
const [rows] = await db.execute(
'SELECT * FROM users WHERE username = ? AND password = ?',
[username, password]
);
if (rows.length > 0) {
res.json({ success: true });
}
});
// Node.js + pg (PostgreSQL)
const { Pool } = require('pg');
const pool = new Pool();
app.get('/user/:id', async (req, res) => {
// БЕЗОПАСНО: параметры через $1, $2, ...
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[req.params.id]
);
res.json(result.rows[0]);
});
2. ORM (Object-Relational Mapping)
ORM автоматически параметризует запросы.
// Prisma
const user = await prisma.user.findUnique({
where: { username: req.body.username },
});
// Sequelize
const user = await User.findOne({
where: {
username: req.body.username,
password: req.body.password,
},
});
// Knex.js (Query Builder)
const user = await knex('users')
.where({ username: req.body.username })
.first;
// НО! Осторожно с raw queries в ORM:
// ОПАСНО:
await sequelize.query(`SELECT * FROM users WHERE name = '${name}'`);
// БЕЗОПАСНО:
await sequelize.query(
'SELECT * FROM users WHERE name = :name',
{ replacements: { name } }
);
3. Валидация ввода
// Проверяем тип и формат ПЕРЕД запросом
const Joi = require('joi');
const loginSchema = Joi.object({
username: Joi.string.alphanum.min(3).max(30).required,
password: Joi.string.min(8).max(100).required,
});
app.post('/login', async (req, res) => {
const { error, value } = loginSchema.validate(req.body);
if (error) return res.status(400).json({ error: error.message });
// value уже валидирован
const [rows] = await db.execute(
'SELECT * FROM users WHERE username = ?',
[value.username]
);
});
4. Принцип минимальных привилегий
-- Создаём пользователя БД с минимальными правами
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Только чтение и запись, без DROP, ALTER, GRANT
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'localhost';
-- Запрещаем FILE, PROCESS, SUPER
-- Даже при SQL injection не сможет читать файлы сервера
5. Экранирование (крайний случай)
// Если параметризация невозможна (динамические имена таблиц):
const mysql = require('mysql2');
// Экранирование значений
const safe = mysql.escape(userInput);
// Экранирование идентификаторов (имён таблиц/колонок)
const safeTable = mysql.escapeId(tableName);
const query = `SELECT * FROM ${safeTable} WHERE id = ?`;
Сравнение: уязвимый vs безопасный код
// ❌ УЯЗВИМО: конкатенация строк
const q = `SELECT * FROM products WHERE name LIKE '%${search}%'`;
// ✅ БЕЗОПАСНО: параметризация
const q = 'SELECT * FROM products WHERE name LIKE ?';
const [rows] = await db.execute(q, [`%${search}%`]);
// ❌ УЯЗВИМО: шаблонные строки
const q = `INSERT INTO users (name, email) VALUES ('${name}', '${email}')`;
// ✅ БЕЗОПАСНО: параметры
const q = 'INSERT INTO users (name, email) VALUES (?, ?)';
await db.execute(q, [name, email]);
// ❌ УЯЗВИМО: динамическая сортировка
const q = `SELECT * FROM users ORDER BY ${req.query.sort()}`;
// ✅ БЕЗОПАСНО: whitelist
const allowedSorts = ['name', 'email', 'created_at'];
const sort = allowedSorts.includes(req.query.sort()) ? req.query.sort() : 'name';
const q = `SELECT * FROM users ORDER BY ${sort}`;
Частые ошибки
1. Доверие клиентской валидации
// Клиентскую валидацию можно обойти!
// ВСЕГДА валидируй на сервере + используй параметризацию
2. Raw queries в ORM
// ПЛОХО: raw query с конкатенацией в Sequelize/Prisma
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE name = '${name}'`);
// ХОРОШО:
await prisma.$queryRaw`SELECT * FROM users WHERE name = ${name}`;
3. Экранирование вместо параметризации
// Экранирование — запасной вариант, не основной
// Параметризация надёжнее, потому что данные
// ФИЗИЧЕСКИ отделены от SQL-кода
Практика
- Создай Express API с уязвимым endpoint и продемонстрируй SQL injection
- Исправь этот endpoint, используя параметризованные запросы (mysql2 или pg)
- Перепиши запросы на Prisma/Sequelize и убедись в защите
- Настрой Joi-валидацию для всех входных параметров API
- Создай пользователя БД с минимальными привилегиями