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


Answers

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

Если вы используете последнюю версию PHP, mysql_real_escape_string ниже опция 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 метод mysql_real_escape_string , вы столкнетесь с проблемой, описанной Polynomial в комментариях ниже. Этот случай более сложный, поскольку целые числа не будут окружены кавычками, поэтому вы можете справиться, подтвердив, что пользовательский ввод содержит только цифры.
  • Вероятно, есть другие случаи, о которых я не знаю. Вы можете обнаружить, что this полезный ресурс по некоторым из более тонких проблем, с которыми вы можете столкнуться.
Question

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

$unsafe_variable = $_POST['user_input']; 

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

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

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

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




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

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

Мой подход:

  • Если вы ожидаете, что ввод будет целым, убедитесь, что он действительно целочисленный. В языке с переменным типом, таком как PHP, это очень важно. Вы можете использовать, например, это очень простое, но мощное решение: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);

  • Если вы ожидаете чего-либо еще от целого шестнадцатеричного значения . Если вы отбросите его, вы полностью избежите ввода. В C / C ++ есть функция mysql_hex_string() , в PHP вы можете использовать bin2hex() .

    Не беспокойтесь о том, что экранированная строка будет иметь размер в 2 раза по mysql_real_escape_string исходной длиной, так как даже если вы используете mysql_real_escape_string , PHP должен выделять одну и ту же емкость ((2*input_length)+1) , что то же самое.

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

Так, например, запрос:

SELECT password FROM users WHERE name = 'root'

Станет:

SELECT password FROM users WHERE name = 0x726f6f74

или

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

Hex - идеальный побег. Никоим образом не вводить.

Разница между функцией UNHEX и префиксом 0x

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

Префикс ** 0x ** может использоваться только для столбцов данных, таких как char, varchar, text, block, binary и т . Д.
Кроме того, его использование немного сложно, если вы собираетесь вставить пустую строку. Вам придется полностью заменить его на '' , или вы получите сообщение об ошибке.

UNHEX () работает в любой колонке; вам не нужно беспокоиться о пустой строке.

Hex-методы часто используются в качестве атак

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

Например, если вы просто делаете что-то вроде этого:

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

атака может ввести вас очень легко . Рассмотрим следующий введенный код, возвращенный из вашего скрипта:

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

и теперь просто извлеките структуру таблицы:

SELECT ... WHERE id = -1 union all select column_name from information_schema.column, где table_name = 0x61727469636c65

А затем просто выберите нужные данные. Разве это не круто?

Но если кодер инъекционного сайта будет шестнадцатеричным, инъекция не будет возможна, потому что запрос будет выглядеть так: SELECT ... WHERE id = UNHEX('2d312075...3635')




I've written this little function several years ago:

function sqlvprintf($query, $args)
{
    global $DB_LINK;
    $ctr = 0;
    ensureConnection(); // Connect to database if not connected already.
    $values = array();
    foreach ($args as $value)
    {
        if (is_string($value))
        {
            $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
        }
        else if (is_null($value))
        {
            $value = 'NULL';
        }
        else if (!is_int($value) && !is_float($value))
        {
            die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
        }
        $values[] = $value;
        $ctr++;
    }
    $query = preg_replace_callback(
        '/{(\\d+)}/', 
        function($match) use ($values)
        {
            if (isset($values[$match[1]]))
            {
                return $values[$match[1]];
            }
            else
            {
                return $match[0];
            }
        },
        $query
    );
    return $query;
}

function runEscapedQuery($preparedQuery /*, ...*/)
{
    $params = array_slice(func_get_args(), 1);
    $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.   
    return $results;
}

This allows running statements in an one-liner C#-ish String.Format like:

runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);

It escapes considering the variable type. If you try to parameterize table, column names, it would fail as it puts every string in quotes which is an invalid syntax.

ОБНОВЛЕНИЕ БЕЗОПАСНОСТИ: предыдущая str_replaceверсия допускала инъекции, добавляя токены {#} в пользовательские данные. Эта preg_replace_callbackверсия не вызывает проблем, если замена содержит эти токены.




A simple way would be to use a PHP framework like CodeIgniter or Laravel which have inbuilt features like filtering and active-record so that you don't have to worry about these nuances.




For those unsure of how to use PDO (coming from the mysql_ functions), I made a very, very simple PDO wrapper that is a single file. It exists to show how easy it is to do all the common things applications need to be done. Works with PostgreSQL, MySQL, and SQLite.

Basically, read it PDO to see how to put the PDO functions to use in real life to make it simple to store and retrieve values in the format you want.

I want a single column

$count = DB::column('SELECT COUNT(*) FROM `user`);

I want an array(key => value) results (ie for making a selectbox)

$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);

I want a single row result

$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));

I want an array of results

$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));



The simple alternative to this problem could be solved by granting appropriate permissions in the database itself. For example: if you are using a mysql database then enter into the database through terminal or the UI provided and just follow this command:

 GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';

This will restrict the user to only get confined with the specified query's only. Remove the delete permission and so the data would never get deleted from the query fired from the php page. The second thing to do is to flush the privileges so that the mysql refreshes the permissions and updates.

FLUSH PRIVILEGES; 

more information about flush .

To see the current privileges for the user fire the following query.

select * from mysql.user where User='username';

Learn more about GRANT .




I think if someone wants to use PHP and MySQL or some other dataBase server:

  1. Think about learning PDO (PHP Data Objects) – it is a database access layer providing a uniform method of access to multiple databases.
  2. Think about learning MySQLi
  3. Use native PHP functions like: strip_tags , mysql_real_escape_string() or if variable numeric, just (int)$foo . Read more about type of variables in PHP here . If you're using libraries such as PDO or MySQLi, always use PDO::quote() and mysqli_real_escape_string() .

Libraries examples:

---- PDO

----- No placeholders - ripe for SQL injection! It's bad

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");

----- Unnamed placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);

----- Named placeholders

$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");

--- MySQLi

$request = $mysqliConnection->prepare('
       SELECT * FROM trainers
       WHERE name = ?
       AND email = ?
       AND last_login > ?');

    $query->bind_param('first_param', 'second_param', $mail, time() - 3600);
    $query->execute();

PS :

PDO wins this battle with ease. With support for twelve different database drivers and named parameters, we can ignore the small performance loss, and get used to its API. From a security standpoint, both of them are safe as long as the developer uses them the way they are supposed to be used

But while both PDO and MySQLi are quite fast, MySQLi performs insignificantly faster in benchmarks – ~2.5% for non-prepared statements, and ~6.5% for prepared ones.

And please test every query to your database - it's a better way to prevent injection.




Предупреждение о безопасности . Этот ответ не соответствует рекомендациям по безопасности. Эвакуация неадекватна для предотвращения внедрения SQL , вместо этого используйте подготовленные инструкции . Используйте стратегию, изложенную ниже, на свой страх и риск. (Кроме того, mysql_real_escape_string() был удален в PHP 7.)

Вы могли бы сделать что-то основное:

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

Это не решит каждую проблему, но это очень хороший шаг. Я оставил очевидные элементы, такие как проверка существования переменной, формат (числа, буквы и т. Д.).




** Warning: the approach described in this answer only applies to very specific scenarios and isn't secure since SQL injection attacks do not only rely on being able to inject X=Y .**

If the attackers are trying to hack into the form via PHP's $_GET variable or with the URL's query string, you would be able to catch them if they're not secure.

RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php

Because 1=1 , 2=2 , 1=2 , 2=1 , 1+1=2 , etc... are the common questions to an SQL database of an attacker. Maybe also it's used by many hacking applications.

But you must be careful, that you must not rewrite a safe query from your site. The code above is giving you a tip, to rewrite or redirect (it depends on you) that hacking-specific dynamic query string into a page that will store the attacker's IP address , or EVEN THEIR COOKIES, history, browser, or any other sensitive information, so you can deal with them later by banning their account or contacting authorities.




A good idea is to use an 'object-relational mapper' like Idiorm :

$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();

$user->first_name = 'Jamie';
$user->save();

$tweets = ORM::for_table('tweet')
    ->select('tweet.*')
    ->join('user', array(
        'user.id', '=', 'tweet.user_id'
    ))
    ->where_equal('user.username', 'j4mie')
    ->find_many();

foreach ($tweets as $tweet) {
    echo $tweet->text;
}

It not only saves you from SQL injections but from syntax errors too! Also Supports collections of models with method chaining to filter or apply actions to multiple results at once and multiple connections.




Параметризованный запрос и проверка ввода - это путь. Существует множество сценариев, в которых может возникать SQL-инъекция, хотя mysql_real_escape_string() используется.

Эти примеры уязвимы для SQL-инъекций:

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

или

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

В обоих случаях вы не можете использовать ' для защиты инкапсуляции.

Source : непредвиденная инъекция SQL (когда Escaping недостаточно)




I favor stored procedures ( MySQL has had stored procedures support since 5.0 ) from a security point of view - the advantages are -

  1. Most databases (including MySQL ) enable user access to be restricted to executing stored procedures. The fine-grained security access control is useful to prevent escalation of privileges attacks. This prevents compromised applications from being able to run SQL directly against the database.
  2. They abstract the raw SQL query from the application so less information of the database structure is available to the application. This makes it harder for people to understand the underlying structure of the database and design suitable attacks.
  3. They accept only parameters, so the advantages of parameterized queries are there. Of course - IMO you still need to sanitize your input - especially if you are using dynamic SQL inside the stored procedure.

The disadvantages are -

  1. They (stored procedures) are tough to maintain and tend to multiply very quickly. This makes managing them an issue.
  2. They are not very suitable for dynamic queries - if they are built to accept dynamic code as parameters then a lot of the advantages are negated.



Я бы рекомендовал использовать PDO (объекты данных PHP) для запуска параметризованных SQL-запросов.

Это не только защищает от SQL-инъекций, но и ускоряет выполнение запросов.

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