Как экранировать SQL-запросы и защитить базу от инъекций
Научитесь экранировать спецсимволы в SQL-запросах, чтобы защитить базу данных от инъекций и утечек информации.
SQL-инъекция остаётся в топ-10 уязвимостей веб-приложений больше 20 лет. Один забытый символ кавычки в запросе — и злоумышленник получает доступ к базе данных. Разбираем, как правильно экранировать SQL и почему это важнее, чем кажется.
Что такое SQL-инъекция и почему она опасна
SQL-инъекция — это атака, при которой злоумышленник вставляет свой SQL-код в запрос через пользовательский ввод. Классический пример:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
Если пользователь введёт в поле логина строку admin' OR '1'='1, запрос станет таким:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = ''
Условие '1'='1' всегда истинно — злоумышленник войдёт без пароля. Это простейший сценарий. В реальности через инъекцию можно удалить данные, украсть информацию о клиентах или получить права администратора.
По данным OWASP, инъекционные атаки входят в тройку самых критичных уязвимостей. Средний ущерб от одной успешной атаки — $200-500 тысяч на восстановление данных и штрафы.
Подготовленные запросы — правильное решение
Забудьте про ручное экранирование. В 2025 году единственный безопасный способ работы с SQL — подготовленные запросы (prepared statements) с параметрами.
PHP (PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->execute([$username, $password]);
$user = $stmt->fetch();
Python (psycopg2):
cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s",
(username, password))
user = cursor.fetchone()
Node.js (pg):
const result = await client.query(
'SELECT * FROM users WHERE username = $1 AND password = $2',
[username, password]
);
Подготовленные запросы отправляют SQL-код и данные отдельно. Сервер базы данных понимает, что параметры — это значения, а не часть кода, и никакие кавычки не помогут изменить логику запроса.
Когда нужно экранирование вручную
Есть три ситуации, когда подготовленные запросы не подходят:
Динамические имена таблиц или колонок
Параметризовать можно только значения, но не имена объектов:
// НЕ РАБОТАЕТ
$stmt = $pdo->prepare("SELECT * FROM ? WHERE status = ?");
$stmt->execute([$tableName, 'active']);
Решение — whitelist разрешённых имён:
$allowedTables = ['users', 'orders', 'products'];
if (!in_array($tableName, $allowedTables)) {
throw new Exception('Invalid table name');
}
$stmt = $pdo->prepare("SELECT * FROM $tableName WHERE status = ?");
$stmt->execute(['active']);
Динамические условия WHERE
Фильтры с переменным количеством условий:
$conditions = [];
$params = [];
if ($category) {
$conditions[] = "category = ?";
$params[] = $category;
}
if ($minPrice) {
$conditions[] = "price >= ?";
$params[] = $minPrice;
}
$sql = "SELECT * FROM products WHERE " . implode(' AND ', $conditions);
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
Легаси-код без поддержки prepared statements
Если работаете со старой библиотекой, используйте встроенные функции экранирования:
MySQL:
$username = $mysqli->real_escape_string($_POST['username']);
$query = "SELECT * FROM users WHERE username = '$username'";
PostgreSQL:
$username = pg_escape_string($connection, $_POST['username']);
$query = "SELECT * FROM users WHERE username = '$username'";
Обязательно оборачивайте значения в кавычки — без них экранирование не защитит от инъекций.
Частые ошибки при экранировании
Ошибка 1: addslashes() вместо специализированных функций
Функция addslashes() в PHP экранирует только базовые символы и не учитывает кодировку базы. Это приводит к уязвимостям при использовании multibyte-кодировок (UTF-8, GBK).
Неправильно:
$username = addslashes($_POST['username']);
Правильно:
$username = $mysqli->real_escape_string($_POST['username']);
Ошибка 2: экранирование LIKE-паттернов
Символы % и _ имеют специальное значение в LIKE-выражениях:
-- Найдёт все записи, если $search = "%"
SELECT * FROM posts WHERE title LIKE '%$search%'
Экранируйте их отдельно:
$search = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $search);
$stmt = $pdo->prepare("SELECT * FROM posts WHERE title LIKE ?");
$stmt->execute(["%$search%"]);
Ошибка 3: забытые числовые параметры
Даже числа нужно параметризовать:
// УЯЗВИМО — пользователь может отправить "1 OR 1=1"
$query = "SELECT * FROM products WHERE id = " . $_GET['id'];
// БЕЗОПАСНО
$stmt = $pdo->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([(int)$_GET['id']]);
Защита на уровне базы данных
Код — не единственная линия обороны. Настройте базу так, чтобы минимизировать последствия возможной инъекции:
Принцип наименьших привилегий. Приложение должно работать от имени пользователя с ограниченными правами. Если приложению не нужно удалять таблицы — не давайте ему DROP права.
-- Создаём пользователя только с правами на чтение/запись
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT, INSERT, UPDATE ON shop.* TO 'app_user'@'localhost';
Разделение учётных записей. Для административных задач используйте отдельного пользователя с полными правами. Для веб-приложения — минимальные права.
Логирование запросов. Включите query log в режиме разработки, чтобы видеть, какие SQL-запросы генерирует приложение:
-- MySQL
SET GLOBAL general_log = 'ON';
SET GLOBAL log_output = 'TABLE';
Инструменты для тестирования и проверки
Не полагайтесь только на ручные проверки. Используйте автоматизацию.
SQLMap — инструмент для автоматического поиска SQL-инъекций. Запустите его на тестовом сервере:
sqlmap -u "http://example.com/product?id=1" --batch --banner
Статические анализаторы кода:
- SonarQube — находит SQL-инъекции в PHP, Java, C#
- Bandit для Python
- ESLint с плагинами для Node.js
Интегрируйте их в CI/CD, чтобы блокировать деплой уязвимого кода.
Если работаете с большим количеством строк для экранирования или нужно быстро проверить безопасность пользовательского ввода, пригодятся универсальные инструменты для работы с текстом. Экранировать JSON полезно при формировании AJAX-запросов, а Экранировать JavaScript — при вставке данных в скрипты. Для работы с CSV-данными загляните в раздел Текст и кодировки.
Чек-лист перед деплоем
Перед выкаткой в продакшен проверьте:
- Все динамические SQL-запросы используют prepared statements или параметризованные запросы
- Имена таблиц и колонок проверяются через whitelist
- Пользователь базы данных имеет минимальные необходимые права
- В коде нет
addslashes(),mysql_escape_string()и других устаревших функций - Включено логирование ошибок SQL без вывода пользователю
- Числовые параметры приводятся к типу перед использованием
- LIKE-паттерны экранируются с учётом
%и_
SQL-инъекции — это решаемая проблема. Используйте подготовленные запросы по умолчанию, а ручное экранирование — только когда это действительно необходимо. Один час на рефакторинг запросов сэкономит месяцы на разбор инцидента.