Как предотвратить SQL-инъекцию в PHP приложениях

SQL-инъекция (SQLi) по-прежнему представляет собой одну из наиболее серьезных и широко распространенных угроз в сфере веб-безопасности. Для разработчиков PHP понимание того, как работает эта уязвимость, и как ее предотвратить, является критически важным для создания безопасных веб-приложений.

Что такое SQL-инъекция, и почему она опасна?

SQL-инъекция происходит, когда данные, предоставленные пользователем, встраиваются непосредственно в SQL-запрос без надлежащих мер безопасности. Вредоносные входящие данные могут изменить SQL-код, что приводит к несанкционированному доступу, краже данных или манипуляциям с содержимым базы данных.

Пример SQL-инъекции

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

Ниже продемонстрирована уязвимая реализация входа в PHP, где любые значения, которые пользователь придает имени пользователя и пароля, непосредственно объединяются с SQL-запросом:

$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

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

Важно: в приложениях в продакшне никогда не следует отправлять, хранить или сравнивать пароли в текстовом формате – нужно сравнивать только хэши паролей.

Если злоумышленник знает о существовании учетной записи по умолчанию с именем admin, для входа достаточно ввести это имя (admin’ —) в качестве логина и указать любой пароль – любое значение, которое примет логика программы. Как следствие SQL-запрос становится таким:

SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'

Символ начинает комментарий, поэтому остальные запросы игнорируются. База данных выполняет:

SELECT * FROM users WHERE username = 'admin'

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

SQLi также может использоваться злоумышленниками для других сценариев атак.

Почему экранирования недостаточно?

Иногда в устаревшем коде для предотвращения SQL-инъекций всё ещё используется mysqli_real_escape_string() для очистки входных данных.

$username = mysqli_real_escape_string($conn, $_GET['user']);
$query = "SELECT * FROM users WHERE name = '$username'";

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

Основы предотвращения SQL-инъекции

Лучшая защита от SQL-инъекций в PHP – это использование параметризированных запросов с подготовленными операторами.

Они отделяют данные пользователя от SQL-кода, гарантируя, что любые специальные символы и ключевые слова SQL во входных пользовательских данных не смогут изменить структуру запроса.

Важно: хотя в обоих примерах для простоты используются жестко закодированные учетные данные, в реальном приложении следует загружать детали подключения к базе данных из переменных среды (с помощью getenv()) или безопасного источника конфигурации, чтобы избежать раскрытия конфиденциальной информации.

Использование PDO (PHP Data Objects)

PDO предоставляет интерфейс для доступа к базам данных. Он поддерживает плейсхолдеры с названиями и встроенную обработку ошибок.

Вот пример запроса на пользователей, соответствующих заданному числовому статусу:

// Hardcoded credentials for illustration only
$pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "pass");
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name AND status = :status");
$stmt->bindParam(':name', $_GET['user'], PDO::PARAM_STR);
$stmt->bindParam(':status', $status, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();

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

Использование MySQLi

MySQLi – это еще один безопасный метод, доступный для выполнения запросов в базу данных в PHP. Он поддерживает позиционные плейсхолдеры и идеально подходит, когда не используется PDO.

Пример с версией PDO для MySQLi:

// Hardcoded credentials for illustration only
$mysqli = new mysqli("localhost", "user", "pass", "testdb"); 

if ($mysqli->connect_error) {
    die("Connection failed: " . $mysqli->connect_error);
}

$stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ? AND status = ?");
$user = $_GET['user'];
$status = 1;
$stmt->bind_param("si", $user, $status); // "s" for string, "i" for integer
$stmt->execute();
$result = $stmt->get_result();

В данном случае функция bind_param() использует плейсхолдеры и обеспечивает корректную обработку типов, тем самым защищая как числовые, так и входные данные строки.

Дополнительные методы предотвращения SQLi в PHP приложениях

Параметризация обязательна, когда речь идет о предотвращении SQL-инъекций, но также соблюдение дополнительных практик сделает приложение более надежным. Однако ни одного из них отдельно недостаточно для защиты от SQLi.

Проверка и очистка входных данных пользователя

Всегда следует проверять входные данные, чтобы убедиться, что они соответствуют ожидаемым типам, форматам и значениям.

Несколько общих правил, которые следует соблюдать:

  • Проверять ожидаемые типы значений. Если ожидается, что параметр должен быть исключительно целым числом, можно применить filter_var($input, FILTER_VALIDATE_INT).
  • Для выбора из закрытого и известного набора применять список разрешенных значений (белый список), чтобы принимать только их.
  • Избегать положения исключительно на фильтрацию определенных ключевых слов (черный список) или удаление специальных символов SQL.

Избегание динамического SQL

Как правило, следует избегать динамического SQL (построение строк запросов на основе входных данных пользователя).

Если это абсолютно необходимо (например, когда присутствуют динамические названия столбцов), следует строго проверять входящие данные или использовать сопоставление с предопределенными значениями, например:

$columns = ['name', 'email', 'created_at'];
$sortColumn = in_array($_GET['sort'], $columns) ? $_GET['sort'] : 'name';
$query = "SELECT * FROM users ORDER BY $sortColumn";

Использование сохраненных процедур с параметрами

Сохраненные процедуры – это предварительно скомпилированные SQL-процедуры, хранящиеся на сервере базы данных. Они могут инкапсулировать сложную логику и при использовании с параметризованными входными данными предоставлять безопасный способ взаимодействия с базой данных.

Вот пример, показывающий, как определить простую сохраненную процедуру в MySQL, которая получает записи пользователей на основе юзернейма. Этот код обычно выполняется непосредственно в клиенте командной строки MySQL или в инструменте управления базами данных, как phpMyAdmin или MySQL Workbench:

CREATE PROCEDURE GetUser(IN uname VARCHAR(50))
BEGIN
    SELECT * FROM users WHERE name = uname;
END

Надо вызвать это из PHP с помощью:

$stmt = $pdo->prepare("CALL GetUser(:uname)");
$stmt->bindParam(':uname', $_GET['user']);
$stmt->execute();

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

Осторожная обработка ошибок

Никогда не следует раскрывать необработанные ошибки базы данных в продакшне. Следует отключить подробные сообщения об ошибках и безопасно их логировать. Это предотвращает получение злоумышленниками информации о структуре запросов или поведении базы данных:

ini_set('display_errors', 0);
error_reporting(0);

// Log errors to a secure file outside the web root
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
error_log("Application error at " . $_SERVER['REQUEST_URI']);

Принцип наименьших привилегий

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

Чтобы ограничить ущерб в случае, если злоумышленнику удастся эксплуатировать инъекцию, не следует разрешать никаких операций, которые не требуются приложению, особенно DROP, GRANT или доступ к административным таблицам.

Тестирование приложений на уязвимости SQL-инъекций

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

Почему DAST важен?

Инструменты динамического тестирования безопасности приложений (DAST) анализируют приложение извне так, как это сделал бы преступник. Они имитируют злоумышленные входные данные в среде выполнения и пытаются эксплуатировать такие уязвимости, как SQL-инъекция, без доступа к исходному коду.

Качественное решение DAST может:

  • Автоматически выявлять точки SQL-инъекций, подвергающиеся эксплуатации, включая слепые SQLi и зависящие от времени. Важно обращать внимание, охватывает ли инструмент все типы SQL-инъекций, как Invicti (ранее Netsparker), ведь пропуск такой уязвимости может дорого стоить компаниям. Если вы хотите бесплатно протестировать это решение, то обратитесь к нам удобным для вас способом.
  • Предоставлять точные результаты, безопасно используя и подтверждая реальные уязвимости, как это делает Invicti, вместо того, чтобы загружать разработчиков ложноположительными результатами.
  • Интегрироваться с CI/CD для автоматического сканирования сборок, а также с тикетными системами для удобства управления результатами.
  • Предлагать практические рекомендации по устранению недостатков и подробности об уязвимости, чтобы помочь командам быстрее решать проблемы.

Таким образом, эти лучшие практики позволяют обеспечить защиту PHP приложений от SQLi, эффективно уменьшая риски безопасности.

Подписаться на новости