Каким образом избежать SQL-инъекций в PHP?

Nicolas Chabanovsky

Если обработка ввода пользователя происходит без преобразования в 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;--')

Что можно сделать, чтобы такого не происходило?

Перевод вопроса «How can I prevent SQL-injection in PHP?» @Andrew G. Johnson.

1 ответ

Nicolas Chabanovsky

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

В общем случае, у вас есть два способа достичь задуманного.

  1. Используйте PDO (для любого поддерживаемого драйвера баз данных):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');

$stmt->execute(array('name' => $name));

foreach ($stmt as $row) {
    // do something with $row
}
  1. Используйте MySQLi (для MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name);

$stmt->execute();

$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
    // do something with $row
}

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

Корректное установление соединения

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

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

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

В приведенном выше примере режим обработки ошибок не является необходимым, но мы настоятельно рекомендуем его использовать. В этом случае при возникновении неполадок сценарий не остановится на Fatal Error. Также это дает разработчику шанс «отловить» ошибку (одну или несколько), которая вызывает исключение PDOException.

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

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

Объяснение

Происходит следующее: SQL-оператор, который вы передаете, чтобы «подготовить», анализируется и компилируется сервером баз данных. Указывая параметры (? или именованный параметр типа :name в вышеприведенном примере), вы сообщаете механизму базы, где вы хотите произвести фильтрацию данных. После этого, когда вы вызываете execute, подготовленный оператор комбинируется с указанными вами значениями параметров.

При этом важно, что значения параметров комбинируются со скомпилированным оператором, а не с SQL-строкой. SQL-внедрение «обманывает» сценарий путем отправки в базу данных вредоносных SQL-строк. Поэтому посылая SQL отдельно от параметров вы уменьшаете риск получения неожиданного и нежелательного результата. Любые параметры, отправляемые при использовании подготовленного оператора, воспринимаются как строки (хотя, конечно, механизм базы данных может произвести определенную оптимизацию и параметры, в конце концов, могут быть преобразованы в численный формат).

В вышеприведенном примере, если переменная $name содержит 'Sarah'; DELETE FROM employees результатом будет поиск строки "'Sarah'; DELETE FROM employees", а не пустая таблица.

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

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

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

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

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

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

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

// Value whitelist
// $dir can only be 'DESC' or 'ASC'
$dir = !empty($direction) ? 'DESC' : 'ASC';

Перевод ответа «How can I prevent SQL-injection in PHP?» @Theo.

licensed under cc by-sa 3.0 with attribution.