Защита от SQL injection: prepared statements и ORM
Защита от SQL injection: prepared statements и ORM
SQL injection — классическая уязвимость из OWASP Top 10: Injection, которая даже в 2026 году регулярно всплывает в production-инцидентах. Одна строка ' OR '1'='1 в поле логина может обойти аутентификацию, украсть всю базу или дать RCE через UDF. В этой статье — типы SQLi, правильная защита через prepared statements, роль ORM, настройка least privilege и fallback-защита через WAF.
Как работает SQL injection
Классическая уязвимость — конкатенация пользовательского ввода в SQL-запрос:
// ❌ Уязвимо
$email = $_POST['email'];
$sql = "SELECT * FROM users WHERE email = '{$email}'";
// Атакующий подставляет: ' OR '1'='1
// Итог: SELECT * FROM users WHERE email = '' OR '1'='1' — вернёт всех
Более опасные payload: '; DROP TABLE users; --, UNION SELECT password, NULL FROM admin_users, blind SQLi через timing attacks.
Типы SQL injection
- Classic (in-band) — результат атаки виден в ответе
- Blind — результат не виден, атакующий отличает true/false по HTTP-статусу или времени ответа
- Out-of-band — эксфильтрация через DNS/HTTP запросы из БД (xp_cmdshell, UTL_HTTP)
- Second-order — значение сохраняется безопасно, но используется в unsafe запросе позже
Главная защита: prepared statements
Prepared statements — разделение SQL-кода и данных на уровне протокола БД. Драйвер БД отправляет запрос-шаблон и параметры отдельно; никакая строка в параметре не может стать SQL-кодом.
PHP PDO
// ✅ Безопасно
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ? AND is_active = ?');
$stmt->execute([$email, 1]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// ✅ Именованные параметры
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $email]);
Критично: отключить эмуляцию prepared в PDO — иначе параметры подставляются на уровне библиотеки строкой:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Node.js
// mysql2
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ? AND active = ?',
[email, 1]
);
// pg
const { rows } = await client.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
Python
# psycopg2
cur.execute('SELECT * FROM users WHERE email = %s', (email,))
# SQLAlchemy Core
stmt = text('SELECT * FROM users WHERE email = :email')
result = conn.execute(stmt, {'email': email})
Что нельзя параметризовать
Имена таблиц, колонок, направление сортировки (ASC/DESC) нельзя передать как параметр. Используйте whitelist:
$allowed = ['name', 'created_at', 'email'];
$sort = in_array($_GET['sort'] ?? '', $allowed, true) ? $_GET['sort'] : 'created_at';
$sql = "SELECT * FROM users ORDER BY {$sort}";
ORM как защита
Современные ORM (Prisma, Doctrine, Sequelize, SQLAlchemy ORM) по умолчанию используют prepared statements. Но опасны методы raw-запросов:
// Prisma — ❌ raw с интерполяцией
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// ✅ Параметризованный tagged template
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
Least privilege для БД-пользователя
Приложение не должно ходить в БД под root. Минимум для приложения:
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'localhost';
-- НЕ давать: DROP, CREATE, ALTER, FILE, SUPER, GRANT
Для read-only эндпоинтов — отдельный пользователь с SELECT only. Это превращает successful SQLi в отчёт вместо катастрофы.
WAF и мониторинг
WAF (ModSecurity + OWASP CRS, Cloudflare) блокирует типичные payload — подробнее в WAF гайде. Настройте алерты на:
- Необычно длинные query-строки (>1 КБ)
- Ключевые слова
UNION,SELECT,DROPво входных данных (не для поиска) - Всплеск 500-ошибок от БД (сигнал SQLi-зондирования)
Проверка
- Статический анализ: Semgrep, SonarQube — найдут конкатенацию в SQL
- Dynamic: sqlmap, Burp Suite Active Scan
- Code review — каждый запрос с переменными
FAQ
Защищает ли mysql_real_escape_string? Слабо. Обходится через multibyte-набор символов (CVE-2006-7243) и не защищает внутри числовых контекстов. Только prepared statements.
Безопасен ли preg_match как фильтр? Нет — атакующий обойдёт любой blacklist. Whitelist ввода + параметризация — единственный workable подход.
Защищает ли ORM полностью? Да, пока вы не используете raw-методы. Проверяйте код на $queryRawUnsafe, db.raw(), Connection.execute().
Что с NoSQL (MongoDB)? Эквивалент — NoSQL injection через объекты $ne/$gt в JSON. Валидируйте тип: typeof user.email === 'string'.
Вывод
Три слоя: 1) prepared statements везде; 2) least privilege в БД; 3) WAF как сигнализация. Мониторьте endpoint'ы через enterno monitors и проверяйте security headers через Security Scanner. Связанное: XSS, rate limiting, WAF.
Проверьте ваш сайт прямо сейчас
Проверить →