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



14 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 полезный ресурс по некоторым из более тонких проблем, с которыми вы можете столкнуться.
sql инъекция

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

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




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

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

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




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

Я столкнулся с этой проблемой, но я думаю, что решил ее очень сложным образом - как хакеры используют, чтобы избежать использования котировок. Я использовал это в сочетании с эмулированными подготовленными заявлениями. Я использую его, чтобы предотвратить всевозможные атаки 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')




Предупреждение о безопасности . Этот ответ не соответствует рекомендациям по безопасности. Эвакуация неадекватна для предотвращения внедрения 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 . "')");

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




Параметризованный запрос и проверка ввода - это путь. Существует множество сценариев, в которых может возникать 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 недостаточно)




Я пользуюсь хранимыми процедурамиMySQL была поддержка хранимых процедур с 5.0 ) с точки зрения безопасности - преимущества -

  1. Большинство баз данных (включая MySQL ) позволяют ограничить доступ пользователей к выполнению хранимых процедур. Четкое управление доступом к безопасности полезно для предотвращения эскалации атак привилегий. Это предотвращает возможность взлома приложений, которые могут быть запущены непосредственно с базой данных.
  2. Они абстрагируют исходный SQL-запрос из приложения, поэтому для приложения доступно меньше информации о структуре базы данных. Это затрудняет понимание людьми базовой структуры базы данных и разработку подходящих атак.
  3. Они принимают только параметры, поэтому существуют преимущества параметризованных запросов. Конечно, IMO вам все равно нужно дезинфицировать ваш вход, особенно если вы используете динамический SQL внутри хранимой процедуры.

Недостатки -

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



Я думаю, что если кто-то захочет использовать PHP и MySQL или какой-нибудь другой сервер базы данных:

  1. Подумайте об обучении PDO (объекты данных PHP) - это уровень доступа к базе данных, обеспечивающий единый метод доступа к нескольким базам данных.
  2. Подумайте об обучении MySQLi
  3. Используйте собственные функции PHP, такие как strip_tags , mysql_real_escape_string() или просто переменные числовые (int)$foo. Подробнее о типах переменных в PHP читайте here . Если вы используете библиотеки, такие как PDO или MySQLi, всегда используйте PDO::quote() и mysqli_real_escape_string() .

Примеры библиотек:

---- PDO

----- Без заполнителей - спелые для SQL-инъекций! Это плохо

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

----- Без названия

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

----- Именованные заполнители

$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 выигрывает эту битву с легкостью. Благодаря поддержке двенадцати различных драйверов баз данных и именованных параметров мы можем игнорировать небольшую потерю производительности и привыкнуть к ее API. С точки зрения безопасности, оба они безопасны, пока разработчик использует их так, как они должны использоваться

Но хотя PDO и MySQLi довольно быстры, MySQLi работает незначительно быстрее в тестах - ~ 2,5% для незаготовленных операторов и ~ 6.5% для готовых.

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




Если вы хотите использовать кэш-движки, такие как Redis или Memcached , возможно, DALMP может быть выбором. Он использует чистый MySQLi . Проверьте это: Уровень абстракции базы данных DALMP для MySQL с использованием PHP.

Кроме того, вы можете «подготовить» свои аргументы перед подготовкой своего запроса, чтобы вы могли создавать динамические запросы и в конце иметь полностью подготовленный запрос. Уровень абстракции базы данных DALMP для MySQL с использованием PHP.




Для тех, кто не знает, как использовать PDO (исходя из mysql_функций), я сделал очень, очень простую PDO-оболочку, которая представляет собой один файл. Он существует, чтобы показать, насколько легко выполнять все обычные приложения, которые необходимо выполнить. Работает с PostgreSQL, MySQL и SQLite.

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

Мне нужен один столбец

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

Я хочу получить результаты массива (key => value) (т. Е. Для создания selectbox)

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

Мне нужен результат с одной строкой

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

Я хочу массив результатов

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



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

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

Это ограничит пользователя только ограничиться только указанным запросом. Удалите разрешение удаления, и поэтому данные никогда не будут удалены из запроса, запущенного с php-страницы. Второе, что нужно сделать, это очистить привилегии, чтобы mysql обновлял разрешения и обновления.

FLUSH PRIVILEGES; 

Дополнительная информация о flush .

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

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

Узнайте больше о GRANT .




Простым способом было бы использовать фреймворк PHP, такой как CodeIgniter или Laravel которые имеют встроенные функции, такие как фильтрация и активная запись, так что вам не нужно беспокоиться об этих нюансах.




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

Если злоумышленники пытаются взломать форму через $_GETпеременную PHP или с помощью строки запроса URL, вы сможете поймать их, если они не защищены.

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

Потому что 1=1, 2=2, 1=2, 2=1, 1+1=2и т.д. ... являются общими вопросами к базе данных SQL злоумышленника. Возможно, он также используется многими хакерскими приложениями.

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




Хорошей идеей является использование «объектно-реляционного картографа», такого как 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;
}

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




Я написал эту небольшую функцию несколько лет назад:

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;
}

Это позволяет запускать операторы в однострочном C # -ish String.Format, например:

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

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

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






Related