Web

Как предотвратить SQL-инъекцию в PHP?

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

$unsafe_variable = $_POST['user_input']; 

 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

 

Это происходит потому, что пользователь может ввести что-то вроде value'); DROP TABLE table;--, и запрос будет выглядеть следующим образом:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

 

Что можно сделать, чтобы этого избежать?

 

Ответ 1

Используйте подготовленные операторы и параметризованные запросыЭто операторы SQL, которые отправляются и анализируются сервером базы данных отдельно от любых параметров. Таким образом, злоумышленник не сможет внедрить вредоносный SQL код.

Есть два варианта достижения этого:

  1. Использование PDO (для любого поддерживаемого драйвера базы данных):

 $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');

 

 $stmt->execute([ 'name' => $name ]);

 

 foreach ($stmt as $row) {

     // некоторый код

 }

 

  1. Использовать MySQLi (для MySQL):

 $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');

 $stmt->bind_param('s', $name); // 's' некоторая переменная строкового типа

 

 $stmt->execute();

 

 $result = $stmt->get_result();

 while ($row = $result->fetch_assoc()) {

     // некоторый код

 }

Если вы подключаетесь к базе данных, отличной от MySQL, есть другой способ для конкретного драйвера, к которому можно обратиться (например,  pg_prepare() и pg_execute() для PostgreSQL).

PDO - универсальный вариант.

Правильная настройка подключения

Обратите внимание, что при использовании PDO для доступа к базе данных MySQL подготовленые операторы по умолчанию не используются. Чтобы исправить это, вам нужно включить эмуляцию этих операторов. Пример создания соединения с использованием PDO:

$dbConnection = new PDO('mysql:dbname=dbtest; host=127.0.0.1; charset=utf8', 'user', 'password');

 

$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

 

В приведенном выше примере активация режима ошибки не является строго обязательной, но рекомендуется использовать ее. Таким образом, скрипт не остановится, на критической ошибке, когда что-то пойдет не так. И это дает разработчику шанс на перехват любой ошибки, которую бросит PDOExceptions.

Однако обязательной является первая setAttribute() строка, которая сообщает PDO отключить эмулируемые операторы и использовать подготовленые  операторы. Это гарантирует, что оператор и его значения не будут проанализированы PHP перед его отправкой на сервер MySQL (что не дает возможному злоумышленнику возможности внедрить вредоносный SQL).

Хотя вы можете установить charset в параметрах конструктора, важно отметить, что «старые» версии PHP (до 5.3.6) игнорировали этот параметр в DSN.

 

Объяснение

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

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

Любые параметры, которые вы отправляете при использовании подготовленного оператора, будут обрабатываться просто как строки (хотя ядро базы данных может выполнять некоторую оптимизацию, поэтому параметры, конечно же, могут оказаться числами). В приведенном выше примере, если $name переменная содержит 'Ваня'; DELETE FROM employees Результатом, будет просто поиск строки "'Ваня'; DELETE FROM employees", и вы не удалите таблицу.

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

Да, и поскольку вы спросили, как это сделать для оператора INSERT INTO, вот пример (с использованием PDO):

$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');

 

$preparedStatement->execute([ 'column' => $unsafeValue ]);

 

Можно ли использовать подготовленные операторы для динамических запросов?

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

// Явный список

// $dir может быть 'DESC' или 'ASC'

if (empty($dir) || $dir !== 'DESC') {

   $dir = 'ASC';

}

 

Ответ 2

Если вы используете последнюю версию PHP, функция mysql_real_escape_string, описанная ниже, больше не будет доступна (mysqli::escape_string является современным эквивалентом). В наши дни функция mysql_real_escape_string имеет смысл только для устаревшего кода в старых версиях PHP.

Существует два варианта применения: экранирование специальных символов в unsafe_variable или использование параметризованного запроса. Оба защитят вас от SQL-инъекций. Параметризованный запрос считается лучшей практикой, но для его использования потребуется перейти на более новое расширение MySQL в PHP.

Сначала мы рассмотрим первый вариант:

// Коннект

 

$unsafe_variable = $_POST["user-input"];

$safe_variable = mysql_real_escape_string($unsafe_variable);

 

mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

 

// Дизконнект

 

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

<?php

    $mysqli = new mysqli("server", "username", "password", "database_name");

    $unsafe_variable = $_POST["user-input"];

    $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");

    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();

?>

 

Ключевая функция, которая используется здесь это mysqli::prepare.

Обратите внимание, что случай, о котором вы спрашивали, довольно простой и что более сложные случаи могут потребовать более сложных подходов. В частности:

  • Если вы хотите изменить структуру SQL на основе пользовательского ввода, параметризованные запросы не помогут, в этом случае необходимо экранировать запрос с помощью mysql_real_escape_string. Лучше передать вводимые пользователем данные через явный список, чтобы обеспечить пропуск только «безопасных» значений.

  • Если вы используете целые числа из пользовательского ввода в условии и применяете функцию mysql_real_escape_string, вы столкнетесь с проблемой, потому что целые числа не будут заключаться в кавычки, и вы не можете быть уверены в корректности формирования запроса.

 

Ответ 3

Каждый ответ здесь охватывает только часть проблемы. Фактически, есть четыре разных частей запроса, которые мы можем динамически добавлять в SQL:

  • строка

  • число

  • идентификатор

  • ключевое слово синтаксиса

 

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

В этом случае каждый динамический параметр должен быть жестко задан в вашем скрипте и выбран из этого набора. Например, чтобы сделать динамический заказ:

$orders  = array("name", "price", "qty"); // Имена полей

$key = array_search($_GET['sort'], $orders)); 

$orderby = $orders[$key]; 

$query = "SELECT * FROM `table` ORDER BY $orderby";

 

Чтобы упростить процесс, я написал вспомогательную функцию, которая выполняет всю работу в одной строке:

$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Не допустимое имя поля");

$query  = "SELECT * FROM `table` ORDER BY `$orderby`";

 

Есть еще один способ защитить идентификаторы – экранирование, но я предпочитаю использовать «белый» список как более надежный и явный подход. Тем не менее, если у вас есть идентификатор в кавычках, вы можете удвоить символ кавычки, чтобы сделать его безопасным.  Тем не менее, существует проблемы с ключевыми словами синтаксиса SQL (такими как ANDDESC и т.п.), но «белый» список кажется единственным подходом в этом случае.

Схожие статьи

Web

Где PHP хранит журнал ошибок?

Web

Сортировка многомерного массива по нескольким столбцам

Web

Как извлечь данные из файла csv в PHP

Web

Как получить значения из выпадающего меню с помощью PHP