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-кода

Практика

  1. Создай Express API с уязвимым endpoint и продемонстрируй SQL injection
  2. Исправь этот endpoint, используя параметризованные запросы (mysql2 или pg)
  3. Перепиши запросы на Prisma/Sequelize и убедись в защите
  4. Настрой Joi-валидацию для всех входных параметров API
  5. Создай пользователя БД с минимальными привилегиями

Связанные темы

Ресурсы