Как пользоваться sqlmap и защитить PHP-код от SQL-инъекций

SQL-инъекция – это уязвимость, при которой пользовательский ввод меняет структуру запроса к базе. Она появляется, когда приложение склеивает SQL из строк и внешних значений. Базовая защита от sql инъекций отделяет значения от SQL-команды. Вы перепишете запросы на PDO и mysqli, затем с помощью sqlmap сравните уязвимый и защищенный обработчики на собственном стенде.

Что такое SQL инъекция и почему она возникает

Запрос к базе состоит из команды и значений. Уязвимый код смешивает эти части в одной строке, поэтому база воспринимает часть пользовательского ввода как SQL-синтаксис.

Небезопасный пример выглядит так:

$email = $_POST['email'] ?? '';
$sql = "SELECT id FROM users WHERE email = '$email'";
$user = $pdo->query($sql)->fetch();

Переменная $email должна содержать адрес. Приложение вставляет ее внутрь SQL без отдельного параметра. Специальные символы во вводе могут изменить условие, добавить другой фрагмент запроса или нарушить его разбор.

Проблема возникает не только в форме входа: значение может прийти из URL, заголовка, JSON-запроса, cookie или сохраненной записи.

SQL-инъекция нарушает границу между кодом и данными: приложение позволяет внешнему значению изменить команду. Проверка формата решает другую задачу: фильтр электронной почты отсеивает ошибочный адрес. Драйвер при этом должен передавать команду и значения раздельно.

Типы инъекций: classic, UNION-based, error-based и blind

Тип атаки зависит от того, как меняется запрос и какой канал возвращает сведения. Названия могут пересекаться: один маршрут способен показывать ошибки и одновременно возвращать измененный результат.

Classic и UNION-based

Classic-инъекция меняет исходное условие запроса. Она встречается там, где приложение добавляет ввод в SQL строковой конкатенацией: в поиске, фильтре или авторизации. UNION-based-инъекция соединяет результат исходного запроса с результатом другой выборки. Число столбцов должно совпадать, а их типы должны быть совместимыми.

Error-based

Error-based-инъекция использует сообщения базы как канал утечки. Подробная ошибка способна раскрыть имена таблиц, поля и часть внутреннего запроса.

Показывайте пользователю короткое техническое сообщение, а подробности сохраняйте в защищенном журнале без паролей, токенов и персональных данных.

Blind: boolean и time-based

Blind-инъекция не возвращает данные напрямую: признаки ищут по различию ответов приложения. Boolean-вариант сравнивает ответы для истинного и ложного условия. Time-based-вариант наблюдает за условной задержкой выполнения запроса.

Сама по себе медленная загрузка не доказывает уязвимость: задержку вызывают сеть, блокировки и нагрузка.

Чем опасна SQL-инъекция

Уязвимый запрос выполняется с правами учетной записи базы. Поэтому последствия зависят от разрешений соединения и возможностей конкретной СУБД.

  • Обход авторизации. Измененное условие может нарушить проверку учетной записи.
  • Чтение данных. Ответ способен раскрыть пользователей, заказы, токены и настройки.
  • Изменение записей. Злоумышленник получает операции, доступные соединению приложения.
  • Удаление данных. Широкие права увеличивают ущерб от измененного запроса.
  • Доступ к ОС. Некоторые настройки СУБД открывают работу с файлами или системными командами.

Минимальные права ограничивают последствия, но не исправляют ошибку в запросе.

Защита от SQL инъекций в PHP с помощью подготовленных запросов

Защита от SQL-инъекций PHP начинается с подготовленного запроса. Сначала задайте SQL-шаблон, затем передайте значения отдельными параметрами. Так работает и PDO, и mysqli.

Сравните примеры SQL-инъекций и защиты от них по формированию SQL до исправления и после него. В небезопасном примере адрес попадает прямо в строку; исправленные варианты передают его через API драйвера.

PDO в PHP

Метод prepare() принимает шаблон с заполнителем :email. Метод execute() передает значение отдельно. Пример предполагает подключение к MySQL и таблицу users с указанными полями.

<?php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=sql_lab;charset=utf8mb4';
$dbUser = 'sql_lab_app';
$dbPassword = 'change-this-password';

$pdo = new PDO($dsn, $dbUser, $dbPassword, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$email = $_POST['email'] ?? null;
if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    http_response_code(400);
    exit('Некорректный адрес электронной почты');
}

$sql = 'SELECT id, email, password_hash
        FROM users
        WHERE email = :email
        LIMIT 1';

$stmt = $pdo->prepare($sql);
$stmt->execute(['email' => $email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Плейсхолдер обозначает значение целиком. Не заключайте :email в кавычки внутри SQL-шаблона. Исключения базы обрабатывайте централизованно, без вывода текста SQL посетителю.

Описание параметров доступно в документации PDO::prepare().

Подготовленный запрос mysqli

В mysqli знак вопроса обозначает параметр. Метод bind_param() связывает переменную с этим параметром.

<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli(
    '127.0.0.1', 'sql_lab_app', 'change-this-password', 'sql_lab', 3306
);
$mysqli->set_charset('utf8mb4');

$email = $_POST['email'] ?? null;
if (!is_string($email) || filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    http_response_code(400);
    exit('Некорректный адрес электронной почты');
}

$sql = 'SELECT id, email, password_hash
        FROM users
        WHERE email = ?
        LIMIT 1';

$stmt = $mysqli->prepare($sql);
$stmt->bind_param('s', $email);
$stmt->execute();
$stmt->bind_result($id, $foundEmail, $passwordHash);

$user = null;
if ($stmt->fetch()) {
    $user = [
        'id' => $id,
        'email' => $foundEmail,
        'password_hash' => $passwordHash,
    ];
}
$stmt->close();

Тип s передает строку, а тип i – целое число. Значение идентификатора проверяйте до передачи в запрос.

Имена таблиц и столбцов нельзя передать через плейсхолдер. Для сортировки сопоставьте пользовательское значение с заранее заданным SQL-фрагментом:

$sort = $_GET['sort'] ?? 'email';
$sortSql = match ($sort) {
    'id' => 'id',
    'email' => 'email',
    default => 'email',
};

$sql = "SELECT id, email FROM users ORDER BY $sortSql";

Переменная $sortSql получает только два значения из кода. Произвольный ввод в ORDER BY не добавляйте.

Тот же принцип на другом стеке

Драйверы Python, Java, .NET и других стеков используют тот же принцип. Например, SQLite в Python получает параметры отдельным кортежем:

import sqlite3

with sqlite3.connect(':memory:') as connection:
    connection.execute(
        'CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL)'
    )
    connection.execute(
        'INSERT INTO users (id, email) VALUES (?, ?)',
        (1, 'alice@example.test')
    )

    email = 'alice@example.test'
    row = connection.execute(
        'SELECT id, email FROM users WHERE email = ?',
        (email,)
    ).fetchone()

    print(row)

Кортеж (email,) содержит одно значение. Запятая сохраняет его как кортеж, а SQL-шаблон остается отдельным. Программа выведет запись с идентификатором 1 и учебным адресом.

Дополнительные меры защиты

Остальные меры уменьшают ущерб и помогают обнаружить ошибку до выпуска.

  • Минимальные права. Учетная запись приложения получает доступ только к нужным таблицам и операциям. Миграции выполняет отдельная учетная запись.
  • Серверная валидация. Проверяйте тип, формат, длину, диапазон и бизнес-правила на сервере.
  • Безопасные ошибки. Пользователь видит короткое сообщение, а журнал хранит технические детали без секретов.
  • ORM и фреймворк. Используйте параметризованный API ORM, а сырые выражения проверяйте отдельно.
  • WAF. Межсетевой экран может отфильтровать часть подозрительных запросов. Ошибку формирования SQL все равно исправляйте в коде.

Экранирование кавычек зависит от контекста, кодировки и режима соединения. Одна пропущенная ветка возвращает риск. Регулярные выражения проверяют формат, но не описывают все диалекты SQL и варианты кодирования.

Клиентская валидация помогает заполнить форму, однако пользователь способен отправить запрос напрямую, минуя HTML-форму и JavaScript.

Рекомендации по разделению команд и данных собраны в памятках OWASP Cheat Sheet Series; защите запросов посвящена памятка SQL Injection Prevention.

Как пользоваться sqlmap для проверки своего кода

sqlmap – консольный инструмент для автоматизированной проверки SQL-инъекций. Тестируйте только свои системы или системы с письменным разрешением владельца. Работайте в изолированном окружении с синтетическими данными.

Проверка через sqlmap включает обнаружение ошибки на учебной цели и повторный анализ после исправления. Для этого создайте два локальных обработчика с одинаковой выборкой.

Характеристикаsqlmap
ТипКонсольный инструмент
Среда запускаИнтерпретатор Python
Назначение в статьеПроверка параметра локального учебного приложения
РезультатОтчет по проверенному маршруту и условиям запуска

Подготовьте учебную базу

Пример рассчитан на установленный локальный MySQL и PHP с расширением pdo_mysql. Проверьте PHP командами php -v и php -m: в списке модулей должно быть pdo_mysql. MySQL должен принимать локальные TCP-подключения на порту 3306.

Откройте терминал и подключитесь клиентом MySQL:

mysql --host=127.0.0.1 --port=3306 --user=root --password

Команда запросит пароль администратора, который вы задали при установке MySQL. Пароль при вводе не отображается. Если вы создали другую административную учетную запись, замените root ее именем.

В Linux с локальной socket-аутентификацией администратора используйте sudo mysql. Эта команда подключает администратора через локальный сокет и может запросить пароль вашей учетной записи ОС. После успешного подключения появится приглашение mysql>.

Вставьте SQL в это приглашение. Замените change-this-password собственным учебным паролем и сохраните его для PHP:

CREATE DATABASE sql_lab CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'sql_lab_app'@'127.0.0.1' IDENTIFIED BY 'change-this-password';
GRANT SELECT ON sql_lab.* TO 'sql_lab_app'@'127.0.0.1';

USE sql_lab;

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(190) NOT NULL,
    password_hash VARCHAR(255) NOT NULL
);

INSERT INTO users (email, password_hash) VALUES
('alice@example.test', 'training-only-value'),
('bob@example.test', 'training-only-value');

SELECT id, email FROM users;

Последний запрос должен показать 2 учебные записи. Введите exit, затем проверьте подключение учетной записи приложения:

mysql --host=127.0.0.1 --port=3306 --user=sql_lab_app --password sql_lab

Введите учебный пароль и выполните SELECT id, email FROM users;. Если появились те же строки, приложение сможет читать таблицу. Завершите клиент командой exit.

Создайте безопасный обработчик

Сохраните файл user.php в отдельной папке. Он сам принимает id, проверяет его и выполняет подготовленный запрос.

<?php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=sql_lab;charset=utf8mb4';
$dbUser = 'sql_lab_app';
$dbPassword = 'change-this-password';

try {
    $pdo = new PDO($dsn, $dbUser, $dbPassword, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);

    $rawId = $_GET['id'] ?? null;
    $id = is_string($rawId)
        ? filter_var($rawId, FILTER_VALIDATE_INT, [
            'options' => ['min_range' => 1],
        ])
        : false;

    if ($id === false) {
        http_response_code(400);
        exit('Некорректный идентификатор');
    }

    $stmt = $pdo->prepare(
        'SELECT id, email FROM users WHERE id = :id'
    );
    $stmt->bindValue(':id', $id, PDO::PARAM_INT);
    $stmt->execute();
    $user = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($user === false) {
        http_response_code(404);
        exit('Запись не найдена');
    }

    header('Content-Type: text/plain; charset=utf-8');
    echo "id: {$user['id']}\nemail: {$user['email']}\n";
} catch (Throwable $exception) {
    http_response_code(500);
    error_log($exception->getMessage());
    exit('Временная ошибка сервера');
}

В $dbPassword укажите пароль учетной записи приложения. Запустите локальный сервер PHP из папки с файлом:

php -S 127.0.0.1:8000

Откройте в браузере http://127.0.0.1:8000/user.php?id=1. Сервер вернет идентификатор и адрес первой учебной записи. Значение id=abc даст ответ HTTP 400.

Добавьте намеренно уязвимую учебную цель

Следующий обработчик намеренно небезопасен. Используйте его только на локальном стенде с учебными данными. Не публикуйте папку в интернете, не настраивайте туннели и не подключайте рабочую базу.

Создайте рядом файл vulnerable.php и укажите тот же учебный пароль:

<?php
// Только для изолированного локального учебного стенда.
$pdo = new PDO(
    'mysql:host=127.0.0.1;port=3306;dbname=sql_lab;charset=utf8mb4',
    'sql_lab_app',
    'change-this-password',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

$id = $_GET['id'] ?? '1';
if (!is_string($id)) {
    http_response_code(400);
    exit('Ожидается строковое значение');
}

// Ошибка: внешнее значение попадает прямо в SQL.
$sql = 'SELECT id, email FROM users WHERE id = ' . $id;

header('Content-Type: text/plain; charset=utf-8');
try {
    $rows = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
    foreach ($rows as $row) {
        echo $row['id'] . ': ' . $row['email'] . "\n";
    }
} catch (PDOException $exception) {
    http_response_code(500);
    exit('Ошибка учебного запроса');
}

Откройте http://127.0.0.1:8000/vulnerable.php?id=1. Страница должна показать первую запись. Здесь проверка is_string() отсеивает массив, однако оставляет возможность изменить SQL строковым вводом.

Для дальнейшей тренировки подойдет намеренно уязвимое приложение DVWA. Его также держите в изоляции от интернета и рабочих данных.

Установите и запустите sqlmap

Скачайте исходники из репозитория проекта sqlmap и распакуйте архив. Откройте второй терминал в полученной папке, где лежит файл sqlmap.py. Первый терминал оставьте для работающего PHP-сервера.

python sqlmap.py -h

В системе, где команда Python называется python3, замените имя интерпретатора. Команда справки выводит список параметров и не обращается к приложению.

Запустите проверку уязвимого локального обработчика:

python sqlmap.py -u "http://127.0.0.1:8000/vulnerable.php?id=1" -p id --batch

Опция -u задает URL, -p id ограничивает проверку параметром id, а --batch выбирает ответы по умолчанию. Эта команда не делает проверку пассивной и не ограничивает нагрузку.

При обнаружении инъекции отчет укажет параметр id и тип найденного канала. Возможная запись Parameter: id (GET) связывает находку с параметром URL. Конкретный набор обнаруженных техник зависит от ответов стенда.

Извлеките учебные строки и проверьте исправление

После обнаружения уязвимости выполните команды против vulnerable.php. Учетная запись приложения имеет право чтения учебной таблицы:

python sqlmap.py -u "http://127.0.0.1:8000/vulnerable.php?id=1" -p id --batch --dbs

python sqlmap.py -u "http://127.0.0.1:8000/vulnerable.php?id=1" -p id --batch -D sql_lab --tables

python sqlmap.py -u "http://127.0.0.1:8000/vulnerable.php?id=1" -p id --batch -D sql_lab -T users -C id,email --dump

--dbs запрашивает доступные базы, -D sql_lab выбирает учебную базу, --tables перечисляет ее таблицы. Параметр -T users выбирает таблицу, -C id,email ограничивает столбцы, а --dump извлекает учебные строки.

В результате ищите базу sql_lab, таблицу users и 2 строки с учебными адресами. Ошибка соединения не считается отрицательным результатом. Если она появилась, откройте адрес в браузере, проверьте работающий PHP-сервер и повторите подключение клиентом MySQL.

Теперь полностью замените содержимое vulnerable.php кодом безопасного user.php, включая правильный пароль. Сохраните файл и повторите анализ того же адреса с очисткой сессии:

python sqlmap.py -u "http://127.0.0.1:8000/vulnerable.php?id=1" -p id --batch --flush-session

Опция --flush-session удаляет сохраненные сведения sqlmap для этой цели. Она не меняет обработчик и не удаляет записи базы. После исправления инструмент не должен обнаруживать прежнюю инъекцию.

Отсутствие находки относится только к проверенному маршруту и условиям запуска.

Завершите работу сервера сочетанием Ctrl+C и удалите учебные PHP-файлы. Для удаления базы подключитесь администратором MySQL и выполните:

DROP DATABASE sql_lab;
DROP USER 'sql_lab_app'@'127.0.0.1';

Частые ошибки при защите от SQL-инъекций

Проверяйте весь путь от входного значения до выполнения запроса и возврата результата.

  • Экранирование вместо параметризации. Контекст, кодировка и режим соединения могут отличаться в соседней ветке.
  • Доверие браузеру. Пользователь обходит JavaScript и отправляет запрос напрямую.
  • Фильтр по словам. Регулярное выражение не покрывает все диалекты SQL и варианты кодирования.
  • Сырые выражения ORM. Строковая конкатенация внутри ORM сохраняет прежний риск.
  • Частичная замена. Небезопасный запрос в другой ветке продолжает принимать внешний ввод.
  • MD5 для паролей. MD5 – это быстрое хеширование, а не шифрование. Для паролей используйте password_hash() и алгоритм bcrypt или Argon2id, если его поддерживает сборка PHP.
  • Широкие права базы. Учетная запись приложения не должна выполнять административные операции.

Хеширование защищает хранение паролей и не исправляет уязвимый SQL-запрос. Даже надежный хеш не поможет, если внешний ввод меняет условие выборки.

Частые вопросы о защите от SQL-инъекций

Совет от нашей редакции: сохраняйте исправленный учебный запрос как образец для проверки остальных обработчиков. Ищите в проекте места, где внешний ввод соединяется с SQL, и заменяйте каждое из них передачей параметров.

Комментарии

Оставьте свой комментарий
Алексей Х.

Щас разбираться ещё,но вроде понял:)спасибо...

VZ
Victor Z.

Подскажите, что делает квадратная скобка в SQLi. Почему без нее не работает инъекция?