ttools
безопасностьsqlэкранированиевеб-разработка

Как экранировать 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-инъекции — это решаемая проблема. Используйте подготовленные запросы по умолчанию, а ручное экранирование — только когда это действительно необходимо. Один час на рефакторинг запросов сэкономит месяцы на разбор инцидента.

Инструменты по теме