Вопрос: Как я могу предотвратить 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;--')

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


2781


источник


Ответы:


Используйте подготовленные заявления и параметризованные запросы. Это операторы 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
    }
    
  2. С помощью MySQLi (для MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $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когда что-то пойдет не так. И это дает разработчику шанс catchлюбые ошибки (ошибки), которые thrown как PDOExceptions.

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

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

объяснение

Что происходит, так это то, что оператор SQL, который вы передаете prepareанализируется и компилируется сервером базы данных. Указав параметры (либо ?или именованный параметр, например :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' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
   $dir = 'ASC';
}

7846



Предупреждение: В этом примере кода ответа (например, примерный код вопроса) используется PHP mysqlрасширение, которое устарело в PHP 5.5.0 и полностью удалено в PHP 7.0.0.

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


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

Мы рассмотрим нижнюю ударную струну, которая будет первой.

//Connect

$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);

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

//Disconnect

См. Также подробную информацию о mysql_real_escape_stringфункция.

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

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

    // TODO - Check that connection was successful.

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

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

    // TODO check that $stmt creation succeeded

    // "s" means the database expects a string
    $stmt->bind_param("s", $unsafe_variable);

    $stmt->execute();

    $stmt->close();

    $mysqli->close();
?>

Ключевая функция, которую вы хотите прочитать, будет mysqli::prepare,

Кроме того, как предложили другие, вам может показаться полезным / легче увеличить уровень абстракции с помощью чего-то вроде PDO ,

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

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

1497



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

  • строка
  • число
  • идентификатор
  • ключевое слово синтаксиса.

и подготовленные заявления охватывают только 2 из них

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

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

$orders  = array("name","price","qty"); //field names
$key     = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe

Однако есть еще один способ защитить идентификаторы - экранирование. Пока вы указываете идентификатор, вы можете избежать обратных звонков внутри, удвоив их.

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

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

Таким образом, общая рекомендация может быть сформулирована как
Пока вы добавляете динамические части в запрос с использованием заполнителей (и эти правильно заполненные заполнители, конечно,), вы можете быть уверены, что ваш запрос безопасен ,

Тем не менее, есть проблема с синтаксическими ключевыми словами SQL (такими как AND, DESCи такой), но белый список - единственный подход в этом случае.

Обновить

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

Например, есть (1) есть (2) до сих пор (3) многие (4) Ответы (5) , в том числе второй самый ответный ответ предлагая вам ручную стиранию строки - устаревший подход, который оказался небезопасным.

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

Я думаю, что все это из-за одного очень старого суеверия, поддержанного такими авторитетами, как OWASP или Руководство PHP , который провозглашает равенство между тем, что «ускользает» и защищается от инъекций SQL.

Независимо от того, что написано в PHP, *_escape_stringникоим образом не делает данные безопасными и никогда не планировалось. Помимо бесполезности для любой части SQL, отличной от строки, ручное экранирование неверно, поскольку оно является ручным, как автоматическое.

И OWASP делает это еще хуже, подчеркивая, что побег ввод пользователя что является полной бессмыслицей: таких слов в контексте защиты от инъекций не должно быть. Каждая переменная потенциально опасна - независимо от источника! Или, другими словами, каждая переменная должна быть должным образом отформатирована для включения в запрос - независимо от источника. Это вопрос назначения. В тот момент, когда разработчик начинает отделять овец от коз (думая, является ли какая-то конкретная переменная «безопасной» или нет), он делает свой первый шаг к катастрофе. Не говоря уже о том, что даже формулировка предполагает, что в точке входа происходит массовое ускорение, напоминающее очень волшебную функцию котировок - уже презренный, устаревший и удаленный.

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

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


927



I'd recommend using PDO (PHP Data Objects) to run parameterized SQL queries.

Not only does this protect against SQL injection, it also speeds up queries.

And by using PDO rather than mysql_, mysqli_, and pgsql_ functions, you make your app a little more abstracted from the database, in the rare occurrence that you have to switch database providers.


762



Use PDO and prepared queries.

($conn is a PDO object)

$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();

561



As you can see, people suggest you use prepared statements at the most. It's not wrong, but when your query is executed just once per process, there would be a slight performance penalty.

I was facing this issue, but I think I solved it in very sophisticated way - the way hackers use to avoid using quotes. I used this in conjunction with emulated prepared statements. I use it to prevent all kinds of possible SQL injection attacks.

My approach:

  • If you expect input to be integer make sure it's really integer. In a variable-type language like PHP it is this very important. You can use for example this very simple but powerful solution: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • If you expect anything else from integer hex it. If you hex it, you will perfectly escape all input. In C/C++ there's a function called mysql_hex_string(), in PHP you can use bin2hex().

    Don't worry about that the escaped string will have a 2x size of its original length because even if you use mysql_real_escape_string, PHP has to allocate same capacity ((2*input_length)+1), which is the same.

  • This hex method is often used when you transfer binary data, but I see no reason why not use it on all data to prevent SQL injection attacks. Note that you have to prepend data with 0x or use the MySQL function UNHEX instead.

So, for example, the query:

SELECT password FROM users WHERE name = 'root'

Will become:

SELECT password FROM users WHERE name = 0x726f6f74

or

SELECT password FROM users WHERE name = UNHEX('726f6f74')

Hex is the perfect escape. No way to inject.

Difference between UNHEX function and 0x prefix

There was some discussion in comments, so I finally want to make it clear. These two approaches are very similar, but they are a little different in some ways:

The ** 0x** prefix can only be used for data columns such as char, varchar, text, block, binary, etc.
Also, its use is a little complicated if you are about to insert an empty string. You'll have to entirely replace it with '', or you'll get an error.

UNHEX() works on any column; you do not have to worry about the empty string.


Hex methods are often used as attacks

Note that this hex method is often used as an SQL injection attack where integers are just like strings and escaped just with mysql_real_escape_string. Then you can avoid the use of quotes.

For example, if you just do something like this:

"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])

an attack can inject you very easily. Consider the following injected code returned from your script:

SELECT ... WHERE id = -1 union all select table_name from information_schema.tables

and now just extract table structure:

SELECT ... WHERE id = -1 union all select column_name from information_schema.column where table_name = 0x61727469636c65

And then just select whatever data ones want. Isn't it cool?

But if the coder of an injectable site would hex it, no injection would be possible because the query would look like this: SELECT ... WHERE id = UNHEX('2d312075...3635')


494



IMPORTANT

The best way to prevent SQL Injection is to use Prepared Statements instead of escaping, as the accepted answer demonstrates.

There are libraries such as Aura.Sql and EasyDB that allow developers to use prepared statements easier. To learn more about why prepared statements are better at stopping SQL injection, refer to this mysql_real_escape_string() bypass and recently fixed Unicode SQL Injection vulnerabilities in WordPress.

Injection prevention - mysql_real_escape_string()

PHP has a specially-made function to prevent these attacks. All you need to do is use the mouthful of a function, mysql_real_escape_string.

mysql_real_escape_string takes a string that is going to be used in a MySQL query and return the same string with all SQL injection attempts safely escaped. Basically, it will replace those troublesome quotes(') a user might enter with a MySQL-safe substitute, an escaped quote \'.

NOTE: you must be connected to the database to use this function!

// Connect to MySQL

$name_bad = "' OR 1'"; 

$name_bad = mysql_real_escape_string($name_bad);

$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";


$name_evil = "'; DELETE FROM customers WHERE 1 or username = '"; 

$name_evil = mysql_real_escape_string($name_evil);

$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;

You can find more details in MySQL - SQL Injection Prevention.


448



Security Warning: This answer is not in line with security best practices. Escaping is inadequate to prevent SQL injection, use prepared statements instead. Use the strategy outlined below at your own risk. (Also, mysql_real_escape_string() was removed in PHP 7.)

You could do something basic like this:

$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");

This won't solve every problem, but it's a very good stepping stone. I left out obvious items such as checking the variable's existence, format (numbers, letters, etc.).


406



Whatever you do end up using, make sure that you check your input hasn't already been mangled by magic_quotes or some other well-meaning rubbish, and if necessary, run it through stripslashes or whatever to sanitize it.


341



Parameterized query AND input validation is the way to go. There are many scenarios under which SQL injection may occur, even though mysql_real_escape_string() has been used.

Those examples are vulnerable to SQL injection:

$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");

or

$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");

In both cases, you can't use ' to protect the encapsulation.

Source: The Unexpected SQL Injection (When Escaping Is Not Enough)


325