Как я могу предотвратить 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;--')
Что можно сделать, чтобы этого не произошло?
Ответы
Используйте подготовленные операторы и параметризованные запросы. Это операторы SQL, которые отправляются и анализируются сервером базы данных отдельно от любых параметров. Таким образом, злоумышленник не сможет внедрить вредоносный SQL.
В основном у вас есть два варианта достижения этого:
Использование PDO (для любого поддерживаемого драйвера базы данных):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
Использование 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', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
В приведенном выше примере режим ошибки не является обязательным, но рекомендуется добавить его . Таким образом, скрипт не остановится, Fatal Error
если что-то пойдет не так. И это дает разработчику шанс на catch
любую ошибку (ы), которые throw
n как PDOException
s.
Однако обязательной является первая setAttribute()
строка, которая сообщает 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([ 'column' => $unsafeValue ]);
Можно ли использовать подготовленные операторы для динамических запросов?
Хотя вы по-прежнему можете использовать подготовленные операторы для параметров запроса, структура самого динамического запроса не может быть параметризована, а некоторые функции запроса не могут быть параметризованы.
Для этих конкретных сценариев лучше всего использовать фильтр белого списка, который ограничивает возможные значения.
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
Устаревшее предупреждение. В примере кода этого ответа (например, в примере кода вопроса) используется
MySQL
расширение PHP , которое было объявлено устаревшим в PHP 5.5.0 и полностью удалено в PHP 7.0.0.Предупреждение о безопасности : этот ответ не соответствует рекомендациям по безопасности. Экранирования недостаточно для предотвращения внедрения SQL , вместо этого используйте подготовленные операторы . Используйте описанную ниже стратегию на свой страх и риск. (Также
mysql_real_escape_string()
было удалено в PHP 7.)
Если вы используете последнюю версию 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
подход, вы столкнетесь с проблемой, описанной Polynomial в комментариях ниже. Этот случай сложнее, потому что целые числа не будут заключаться в кавычки, поэтому вы можете справиться, проверив, что вводимые пользователем данные содержат только цифры. - Вероятно, есть и другие случаи, о которых я не знаю. Вы можете найти этот полезный ресурс по некоторым из более тонких проблем, с которыми вы можете столкнуться.
Каждый ответ здесь охватывает только часть проблемы. Фактически, есть четыре различных части запроса, которые мы можем добавлять в SQL динамически:
- строка
- число
- идентификатор
- ключевое слово синтаксиса
И подготовленные заявления охватывают только два из них.
Но иногда нам приходится делать наш запрос еще более динамичным, добавляя в него операторы или идентификаторы. Итак, нам потребуются разные техники защиты.
В целом такой подход к защите основан на добавлении в белый список .
В этом случае каждый динамический параметр должен быть жестко задан в вашем скрипте и выбран из этого набора. Например, чтобы сделать динамический заказ:
$orders = array("name", "price", "qty"); // Field names $key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically. $query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe
Чтобы упростить процесс, я написал вспомогательную функцию белого списка, которая выполняет всю работу в одной строке:
$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name"); $query = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe
Есть еще один способ защитить идентификаторы - избежать, но я предпочитаю использовать белый список как более надежный и явный подход. Тем не менее, если у вас есть идентификатор в кавычках, вы можете избежать символа кавычки, чтобы сделать его безопасным. Например, по умолчанию для mysql вам нужно удвоить символ кавычки, чтобы избежать его . Для других СУБД правила экранирования будут другими.
Тем не менее, существует проблема с ключевыми словами синтаксиса SQL (такими как AND
, DESC
и т.п.), но белый список кажется единственным подходом в этом случае.
Итак, общая рекомендация может быть сформулирована как
- Любая переменная, представляющая литерал данных SQL (или, проще говоря - строку SQL или число), должна быть добавлена через подготовленный оператор. Без исключений.
- Любая другая часть запроса, такая как ключевое слово SQL, таблица или имя поля, или оператор, должна быть отфильтрована через белый список.
Обновить
Несмотря на то, что существует общее согласие относительно передовых методов защиты от SQL-инъекций, все еще существует много плохих практик. И некоторые из них слишком глубоко укоренились в сознании пользователей PHP. Например, на этой самой странице (хотя и невидимо для большинства посетителей) более 80 удаленных ответов - все они удалены сообществом из-за плохого качества или пропаганды плохих и устаревших методов. Что еще хуже, некоторые из плохих ответов не удаляются, а скорее процветают.
Например, есть (1) (2) все еще (3) много (4) ответов (5) , включая второй наиболее набранный ответ, предлагающий вам экранирование строки вручную - устаревший подход, который оказался небезопасным.
Или есть немного лучший ответ, который предлагает просто еще один метод форматирования строк и даже может похвастаться им как конечной панацеей. Хотя, конечно, это не так. Этот метод не лучше обычного форматирования строк, но при этом сохраняет все свои недостатки: он применим только к строкам и, как и любое другое ручное форматирование, является необязательной, необязательной мерой, подверженной человеческим ошибкам любого рода.
Я думаю, что все это из-за одного очень старого суеверия, поддерживаемого такими авторитетами, как OWASP или руководство по PHP , которое провозглашает равенство между любым «бегством» и защитой от SQL-инъекций.
Независимо от того, что было сказано в руководстве по PHP на протяжении многих лет, *_escape_string
ни в коем случае не делает данные безопасными и никогда не предназначалось для этого. Помимо бесполезности для какой-либо части SQL, кроме строки, экранирование вручную неверно, потому что оно выполняется вручную в отличие от автоматического.
И OWASP делает это еще хуже, делая упор на экранирование пользовательского ввода, что является полной ерундой: таких слов не должно быть в контексте защиты от инъекций. Каждая переменная потенциально опасна, независимо от источника! Или, другими словами, каждая переменная должна быть правильно отформатирована, чтобы быть помещенной в запрос, независимо от источника. Главное - место назначения. В тот момент, когда разработчик начинает отделять овец от коз (думая, является ли какая-то конкретная переменная «безопасной» или нет), он / она делает свой первый шаг к катастрофе. Не говоря уже о том, что даже формулировка предполагает массовое экранирование в точке входа, напоминающее очень волшебную функцию кавычек - уже презираемую, устаревшую и удаленную.
Итак, в отличие от любого «экранирования», подготовленные операторы - это мера, которая действительно защищает от SQL-инъекции (если применимо).
Я бы рекомендовал использовать PDO (объекты данных PHP) для выполнения параметризованных запросов SQL.
Это не только защищает от SQL-инъекций, но также ускоряет запросы.
И с помощью PDO , а не mysql_
, mysqli_
и pgsql_
функции, вы сделаете ваше приложение немного абстрагируется из базы данных, в редком возникновении что вы должны поставщикам база данных коммутаторов.
Используйте PDO
и подготовленные запросы.
( $conn
это PDO
объект)
$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)"); $stmt->bindValue(':id', $id); $stmt->bindValue(':name', $name); $stmt->execute();
Как видите, люди предлагают использовать в лучшем случае заранее подготовленные операторы. В этом нет ничего плохого, но когда ваш запрос выполняется только один раз для каждого процесса, производительность будет незначительной.
Я столкнулся с этой проблемой, но думаю, что решил ее очень изощренным способом - способом, которым хакеры избегают использования кавычек. Я использовал это вместе с эмулированными подготовленными операторами. Я использую его , чтобы предотвратить все виды возможных атак с внедрением 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
, PHP должен выделить((2*input_length)+1)
такую же емкость , что и будет.Этот шестнадцатеричный метод часто используется при передаче двоичных данных, но я не вижу причин, почему бы не использовать его для всех данных, чтобы предотвратить атаки SQL-инъекций. Обратите внимание, что вы должны добавить данные
0x
или использоватьUNHEX
вместо них функцию MySQL .
Так, например, запрос:
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 WHERE table_name = __0x61727469636c65__;
А затем просто выберите те данные, которые вам нужны. Разве это не круто?
Но если бы кодировщик инъекционного сайта заколдовал бы его, внедрение было бы невозможно, потому что запрос будет выглядеть так:
SELECT ... WHERE id = UNHEX('2d312075...3635');
Устаревшее предупреждение. В примере кода этого ответа (например, в примере кода вопроса) используется
MySQL
расширение PHP , которое было объявлено устаревшим в PHP 5.5.0 и полностью удалено в PHP 7.0.0.Предупреждение о безопасности : этот ответ не соответствует рекомендациям по безопасности. Экранирования недостаточно для предотвращения внедрения SQL , вместо этого используйте подготовленные операторы . Используйте описанную ниже стратегию на свой страх и риск. (Также
mysql_real_escape_string()
было удалено в PHP 7.)ВАЖНЫЙ
Лучший способ предотвратить SQL-инъекцию - использовать подготовленные операторы вместо экранирования , как показывает принятый ответ .
Существуют библиотеки, такие как Aura.Sql и EasyDB, которые позволяют разработчикам проще использовать подготовленные операторы. Чтобы узнать больше о том, почему подготовленные операторы лучше останавливают SQL-инъекцию , обратитесь к этому mysql_real_escape_string()обходу и недавно исправленным уязвимостям Unicode SQL Injection в WordPress .
Предотвращение инъекций - mysql_real_escape_string ()
В PHP есть специальная функция для предотвращения этих атак. Все, что вам нужно сделать, это использовать функцию mysql_real_escape_string
.
mysql_real_escape_string
принимает строку, которая будет использоваться в запросе MySQL, и возвращает ту же строку с безопасным экранированием всех попыток внедрения SQL. По сути, он заменит те вызывающие беспокойство кавычки ('), которые пользователь может ввести, на безопасную для MySQL замену, экранированную кавычку \'.
ПРИМЕЧАНИЕ: вы должны быть подключены к базе данных, чтобы использовать эту функцию!
// Подключаемся к 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;
Вы можете найти более подробную информацию в MySQL - SQL Injection Prevention .
Вы можете сделать что-нибудь базовое, например:
$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection); mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
Это не решит все проблемы, но это очень хорошая ступенька. Я не учел очевидные моменты, такие как проверка существования переменной, ее формата (числа, буквы и т. Д.).
Независимо от того, что вы в конечном итоге используете, убедитесь, что вы проверяете, что ваш ввод еще не был искажен magic_quotes
или какой-либо другой благонамеренный мусор, и, если необходимо, пропустите его stripslashes
или что-то еще, чтобы очистить его.
Устаревшее предупреждение. В примере кода этого ответа (например, в примере кода вопроса) используется
MySQL
расширение PHP , которое было объявлено устаревшим в PHP 5.5.0 и полностью удалено в PHP 7.0.0.Предупреждение о безопасности : этот ответ не соответствует рекомендациям по безопасности. Экранирования недостаточно для предотвращения внедрения SQL , вместо этого используйте подготовленные операторы . Используйте описанную ниже стратегию на свой страх и риск. (Также
mysql_real_escape_string()
было удалено в PHP 7.)
Параметризованный запрос И проверка ввода - лучший способ. Есть много сценариев, при которых может произойти 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`");
В обоих случаях вы не можете использовать '
для защиты инкапсуляции.
Источник : Неожиданная SQL-инъекция (когда побега недостаточно)
На мой взгляд, лучший способ вообще предотвратить SQL-инъекцию в вашем приложении PHP (или любом другом веб-приложении, если на то пошло) - это подумать об архитектуре вашего приложения. Если единственный способ защититься от SQL-инъекций - не забывать использовать специальный метод или функцию, которая делает все правильно каждый раз, когда вы разговариваете с базой данных, вы делаете это неправильно. Таким образом, когда вы забудете правильно отформатировать запрос в какой-то момент кода, это всего лишь вопрос времени.
Принятие шаблона MVC и фреймворка, такого как CakePHP или CodeIgniter , вероятно, является правильным путем: общие задачи, такие как создание защищенных запросов к базе данных, были решены и централизованно реализованы в таких фреймворках. Они помогают разумно организовать ваше веб-приложение и заставляют думать больше о загрузке и сохранении объектов, чем о безопасном построении отдельных запросов SQL.
Есть много способов предотвратить SQL-инъекции и другие взломы SQL. Вы можете легко найти его в Интернете (поиск Google). Конечно, PDO - одно из хороших решений. Но я хотел бы предложить вам хорошую защиту ссылок от SQL-инъекции.
Что такое SQL-инъекция и как предотвратить
Руководство по PHP для SQL-инъекций
Объяснение Microsoft SQL-инъекций и предотвращения в PHP
И некоторые другие, такие как Предотвращение SQL-инъекций с помощью MySQL и PHP .
Итак, почему вам нужно предотвратить ваш запрос от SQL-инъекции?
Я хотел бы сообщить вам: почему мы пытаемся предотвратить SQL-инъекцию с помощью небольшого примера ниже:
Запрос на совпадение аутентификации при входе:
$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";
Теперь, если кто-то (хакер) поставит
$_POST['email']= [email protected]' OR '1=1
и пароль ничего ....
Запрос будет проанализирован в системе только до:
$query="select * from users where email='[email protected]' OR '1=1';
Другая часть будет отброшена. Итак, что будет? Неавторизованный пользователь (хакер) сможет войти в систему как администратор, не имея своего пароля. Теперь он / она может делать все, что может сделать администратор / адрес электронной почты. Видите ли, это очень опасно, если не предотвратить SQL-инъекцию.
Я предпочитаю хранимые процедуры ( MySQL поддерживает хранимые процедуры с 5.0 ) с точки зрения безопасности - преимущества:
- Большинство баз данных (включая MySQL ) позволяют ограничить доступ пользователей к выполнению хранимых процедур. Детальный контроль доступа к системе безопасности полезен для предотвращения атак, связанных с повышением привилегий. Это не позволяет скомпрометированным приложениям запускать SQL непосредственно в базе данных.
- Они абстрагируют необработанный SQL-запрос от приложения, поэтому приложению доступно меньше информации о структуре базы данных. Это затрудняет понимание базовой структуры базы данных и разработку подходящих атак.
- Они принимают только параметры, поэтому преимущества параметризованных запросов есть. Конечно - IMO вам все еще нужно дезинфицировать свой ввод - особенно если вы используете динамический SQL внутри хранимой процедуры.
Недостатки -
- Их (хранимые процедуры) сложно поддерживать, и они очень быстро размножаются. Это делает управление ими проблемой.
- Они не очень подходят для динамических запросов - если они построены так, чтобы принимать динамический код в качестве параметров, многие преимущества теряются.
Думаю, если кто-то захочет использовать PHP и MySQL или какой-нибудь другой сервер базы данных:
- Подумайте об изучении PDO (объекты данных PHP) - это уровень доступа к базе данных, обеспечивающий единый метод доступа к нескольким базам данных.
- Подумайте об изучении MySQLi
- Используйте собственные функции PHP, такие как strip_tags , mysql_real_escape_string или, если числовая переменная, просто
(int)$foo
. Подробнее о типах переменных в PHP здесь . Если вы используете такие библиотеки, как 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% для подготовленных.
И, пожалуйста, проверяйте каждый запрос к своей базе данных - это лучший способ предотвратить внедрение.
Если возможно, приведите типы ваших параметров. Но он работает только с простыми типами, такими как int, bool и float.
$unsafe_variable = $_POST['user_id'];
$safe_variable = (int)$unsafe_variable ;
mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
Если вы хотите использовать механизмы кэширования, такие как Redis или Memcached , возможно, вам подойдет DALMP. Он использует чистый MySQLi . Проверьте это: Уровень абстракции базы данных DALMP для MySQL с использованием PHP.
Кроме того, вы можете «подготовить» свои аргументы перед подготовкой вашего запроса, чтобы вы могли создавать динамические запросы и в конце иметь полностью подготовленный запрос операторов. Уровень абстракции базы данных DALMP для MySQL с использованием PHP.
Для тех, кто не знает, как использовать PDO (исходя из mysql_
функций), я сделал очень и очень простую оболочку PDO, которая представляет собой один файл. Он существует для того, чтобы показать, насколько легко делать все, что нужно приложению. Работает с PostgreSQL, MySQL и SQLite.
По сути, прочтите его, пока вы читаете руководство, чтобы увидеть, как использовать функции PDO в реальной жизни, чтобы упростить сохранение и получение значений в нужном вам формате .
Я хочу одну колонку
$count = DB::column('SELECT COUNT(*) FROM `user`);
Мне нужен массив (ключ => значение) результатов (т.е. для создания поля выбора)
$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));
Используя эту функцию PHP, mysql_escape_string()
вы можете быстро получить хорошую профилактику.
Например:
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'
mysql_escape_string
- Экранирует строку для использования в mysql_query
Для большей профилактики вы можете добавить в конце ...
wHERE 1=1 or LIMIT 1
В итоге вы получите:
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
Несколько рекомендаций по экранированию специальных символов в операторах SQL.
Не используйте MySQL . Это расширение устарело. Вместо этого используйте MySQLi или PDO .
MySQLi
Чтобы вручную экранировать специальные символы в строке, вы можете использовать функцию mysqli_real_escape_string . Функция не будет работать должным образом, если правильный набор символов не установлен с помощью mysqli_set_charset .
Пример:
$mysqli = new mysqli('host', 'user', 'password', 'database'); $mysqli->set_charset('charset');
$string = $mysqli->real_escape_string($string); $mysqli->query("INSERT INTO table (column) VALUES ('$string')");
Для автоматического экранирования значений с помощью подготовленных операторов используйте mysqli_prepare и mysqli_stmt_bind_param, где типы для соответствующих переменных связывания должны быть предоставлены для соответствующего преобразования:
Пример:
$stmt = $mysqli->prepare("INSERT INTO table (column1, column2) VALUES (?,?)"); $stmt->bind_param("is", $integer, $string);
$stmt->execute();
Независимо от того, используете ли вы подготовленные операторы или mysqli_real_escape_string
, вам всегда нужно знать тип входных данных, с которыми вы работаете.
Поэтому, если вы используете подготовленный оператор, вы должны указать типы переменных для mysqli_stmt_bind_param
функции.
И mysqli_real_escape_string
, как следует из названия, используется для экранирования специальных символов в строке, поэтому это не сделает целые числа безопасными. Цель этой функции - предотвратить разрыв строк в операторах SQL и повреждение базы данных, которое это может вызвать. mysqli_real_escape_string
- полезная функция при правильном использовании, особенно в сочетании с sprintf
.
Пример:
$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0'; $query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer); echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5
$integer = '99999999999999999999'; $query = sprintf("SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string($string), $integer); echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647
Простая альтернатива этой проблеме может быть решена путем предоставления соответствующих разрешений в самой базе данных. Например: если вы используете базу данных MySQL, войдите в базу данных через терминал или предоставленный пользовательский интерфейс и просто выполните эту команду:
GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';
Это ограничит пользователя, чтобы он ограничивался только указанным запросом. Удалите разрешение на удаление, и данные никогда не будут удалены из запроса, запущенного со страницы PHP. Второе, что нужно сделать, - сбросить привилегии, чтобы MySQL обновил разрешения и обновления.
FLUSH PRIVILEGES;
больше информации о промывке .
Чтобы увидеть текущие привилегии для пользователя, выполните следующий запрос.
select * from mysql.user where User='username';
Узнайте больше о GRANT .
Что касается многих полезных ответов, я надеюсь добавить некоторую ценность этой теме.
SQL-инъекция - это атака, которая может быть выполнена с помощью пользовательского ввода (вводимые пользователем данные, а затем используются внутри запросов). Шаблоны SQL-инъекций - это правильный синтаксис запросов, в то время как мы можем назвать это: плохие запросы по плохим причинам, и мы предполагаем, что может быть плохой человек, который пытается получить секретную информацию (в обход управления доступом), которая влияет на три принципа безопасности (конфиденциальность , целостность и доступность).
Теперь наша цель - предотвратить угрозы безопасности, такие как атаки SQL-инъекций, задаваемый вопрос (как предотвратить атаки SQL-инъекций с помощью PHP), быть более реалистичным, фильтрация данных или очистка входных данных происходит при использовании данных, вводимых пользователем внутри такой запрос с использованием PHP или любого другого языка программирования не является случаем, или, как рекомендуют другие люди, использовать современные технологии, такие как подготовленный оператор или любые другие инструменты, которые в настоящее время поддерживают предотвращение SQL-инъекций, считаете, что эти инструменты больше недоступны? Как вы защищаете свое приложение?
Мой подход к SQL-инъекциям: очистка вводимых пользователем данных перед их отправкой в базу данных (перед использованием в любом запросе).
Фильтрация данных для (преобразование небезопасных данных в безопасные)
Учтите, что PDO и MySQLi недоступны. Как можно защитить свое приложение? Вы заставляете меня использовать их? А как насчет других языков, кроме PHP? Я предпочитаю давать общие идеи, так как их можно использовать для более широкой границы, а не только для определенного языка.
- Пользователь SQL (ограничение прав пользователя): наиболее распространенными операциями SQL являются (SELECT, UPDATE, INSERT), тогда зачем давать привилегию UPDATE пользователю, который этого не требует? Например, страницы входа и поиска используют только SELECT, тогда зачем использовать на этих страницах пользователей БД с высокими привилегиями?
ПРАВИЛО: не создавайте одного пользователя базы данных для всех привилегий. Для всех операций SQL вы можете создать свою схему, например (deluser, selectuser, updateuser) в качестве имен пользователей для удобства использования.
См. Принцип наименьших привилегий .
Фильтрация данных: перед созданием любого запроса, вводимого пользователем, его необходимо проверить и отфильтровать. Для программистов важно определить некоторые свойства для каждой переменной, вводимой пользователем: тип данных, шаблон данных и длина данных . Поле, которое представляет собой число между (x и y), должно быть точно проверено с использованием точного правила, а для поля, которое представляет собой строку (текст): шаблон - это случай, например, имя пользователя должно содержать только некоторые символы, давайте скажи [a-zA-Z0-9_-.]. Длина варьируется от (x до n), где x и n (целые числа, x <= n). Правило: создание точных фильтров и правил проверки - лучший способ для меня.
Используйте другие инструменты: Здесь я также соглашусь с вами, что подготовленный оператор (параметризованный запрос) и хранимые процедуры. Недостатки здесь в том, что эти способы требуют продвинутых навыков, которых нет у большинства пользователей. Основная идея здесь состоит в том, чтобы различать SQL-запрос и данные, которые используются внутри. Оба подхода можно использовать даже с небезопасными данными, поскольку введенные пользователем данные ничего не добавляют к исходному запросу, например (any или x = x).
Для получения дополнительной информации, пожалуйста, прочтите шпаргалку OWASP SQL Injection Prevention .
Теперь, если вы опытный пользователь, начните использовать эту защиту по своему усмотрению, но для новичков, если они не могут быстро реализовать хранимую процедуру и подготовить инструкцию, лучше фильтровать входные данные настолько, насколько они могут.
Наконец, давайте предположим, что пользователь отправляет этот текст ниже вместо того, чтобы вводить свое имя пользователя:
[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'
Эти входные данные можно проверить на ранней стадии без каких-либо подготовленных операторов и хранимых процедур, но на всякий случай их использование начинается после фильтрации и проверки пользовательских данных.
Последний пункт - обнаружение неожиданного поведения, которое требует больших усилий и сложности; это не рекомендуется для обычных веб-приложений.
Неожиданное поведение в указанном выше пользовательском вводе: SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA и root. Как только эти слова обнаружены, вы можете избежать ввода.
ОБНОВЛЕНИЕ 1:
Пользователь прокомментировал, что этот пост бесполезен, ОК! Вот что предоставил OWASP.ORG :
Основные меры защиты:
Вариант №1: использование подготовленных операторов (параметризованные запросы)
Вариант №2: использование хранимых процедур
Вариант №3: экранирование всех вводимых пользователем данных
Дополнительные средства защиты:
также принудительное применение: минимальные привилегии
Также выполнение: проверка ввода белого списка
Как вы, возможно, знаете, утверждение статьи должно быть подтверждено веским аргументом, по крайней мере, одной ссылкой! В противном случае это считается нападением и плохой претензией!
Обновление 2:
Из руководства PHP, PHP: Подготовленные операторы - Руководство :
Экранирование и SQL-инъекция
Связанные переменные будут автоматически экранированы сервером. Сервер вставляет их экранированные значения в соответствующие места в шаблон оператора перед выполнением. Подсказка должна быть предоставлена серверу для типа связанной переменной, чтобы создать соответствующее преобразование. См. Функцию mysqli_stmt_bind_param () для получения дополнительной информации.
Автоматическое экранирование значений на сервере иногда считается функцией безопасности для предотвращения внедрения SQL. Такая же степень безопасности может быть достигнута с помощью неподготовленных операторов, если входные значения экранированы правильно.
Обновление 3:
Я создал тестовые примеры, чтобы узнать, как PDO и MySQLi отправляют запрос на сервер MySQL при использовании подготовленного оператора:
PDO:
$user = "''1''"; // Malicious keyword $sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)); $sth->execute(array(':username' => $user));
Журнал запросов:
189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\'' 189 Quit
MySQLi:
$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) { $stmt->bind_param("s", $user); $user = "''1''";
$stmt->execute();
Журнал запросов:
188 Prepare SELECT * FROM awa_user WHERE username =? 188 Execute SELECT * FROM awa_user WHERE username ='\'\'1\'\'' 188 Quit
Понятно, что подготовленный оператор также экранирует данные, ничего больше.
Как также упоминалось в приведенном выше заявлении,
Автоматическое экранирование значений на сервере иногда считается функцией безопасности для предотвращения внедрения SQL. Такая же степень безопасности может быть достигнута с неподготовленными операторами, если входные значения экранированы правильно.
Следовательно, это доказывает, что проверка данных, например, intval()
является хорошей идеей для целочисленных значений перед отправкой любого запроса. Кроме того, предотвращение злонамеренных данных пользователя перед отправкой запроса - правильный и действенный подход .
См. Этот вопрос для более подробной информации: PDO отправляет необработанный запрос в MySQL, в то время как Mysqli отправляет подготовленный запрос, оба дают одинаковый результат
Рекомендации:
- Памятка по внедрению SQL
- SQL-инъекция
- Информационной безопасности
- Принципы безопасности
- Проверка данных
Предупреждение о безопасности : этот ответ не соответствует рекомендациям по безопасности. Экранирования недостаточно для предотвращения внедрения SQL , вместо этого используйте подготовленные операторы . Используйте описанную ниже стратегию на свой страх и риск. (Также
mysql_real_escape_string()
было удалено в PHP 7.)Устарело Предупреждение : расширение mysql в настоящее время не рекомендуется. мы рекомендуем использовать расширение PDO
Я использую три разных способа защиты моего веб-приложения от SQL-инъекции.
- Использование
mysql_real_escape_string()
, которое является предварительно определенной функцией в PHP , и этот код оного обратный слеш следующих символы:\x00
,\n
,\r
,\
,'
,"
и\x1a
. Передайте входные значения в качестве параметров, чтобы минимизировать вероятность SQL-инъекции. - Самый продвинутый способ - использовать PDO.
Я надеюсь, что это поможет вам.
Рассмотрим следующий запрос:
$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string () здесь не защищает. Если вы используете одинарные кавычки ('') вокруг ваших переменных внутри вашего запроса, это то, что защищает вас от этого. Вот решение для этого ниже:
$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";
На этот вопрос есть несколько хороших ответов по этому поводу.
Я предполагаю, что использование PDO - лучший вариант.
Редактировать:
mysql_real_escape_string()
устарела с PHP 5.5.0. Используйте mysqli или PDO.
Альтернативой mysql_real_escape_string () является
string mysqli_real_escape_string ( mysqli $link , string $escapestr )
Пример:
$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");
Простым способом было бы использовать фреймворк 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-адрес злоумышленника или ДАЖЕ ИХ КУКИ, история, браузер или любой другой конфиденциальный информации, чтобы вы могли разобраться с ними позже, заблокировав их учетную запись или связавшись с властями.
Хорошая идея - использовать объектно-реляционный картограф, например 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-инъекций, но и от синтаксических ошибок! Он также поддерживает коллекции моделей с цепочкой методов для фильтрации или применения действий сразу к нескольким результатам и нескольким соединениям.
Существует так много ответов для PHP и MySQL , но вот код для PHP и Oracle для предотвращения SQL-инъекций, а также регулярного использования драйверов oci8:
$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);
Устаревшее предупреждение. В примере кода этого ответа (например, в примере кода вопроса) используется
MySQL
расширение PHP , которое было объявлено устаревшим в PHP 5.5.0 и полностью удалено в PHP 7.0.0.Предупреждение о безопасности : этот ответ не соответствует рекомендациям по безопасности. Экранирования недостаточно для предотвращения внедрения SQL , вместо этого используйте подготовленные операторы . Используйте описанную ниже стратегию на свой страх и риск. (Также
mysql_real_escape_string()
было удалено в PHP 7.)
Использование PDO и MYSQLi - хорошая практика для предотвращения инъекций SQL, но если вы действительно хотите работать с функциями и запросами MySQL, было бы лучше использовать
mysql_real_escape_string
$unsafe_variable = mysql_real_escape_string($_POST['user_input']);
Есть больше возможностей предотвратить это: например, определить - если входные данные являются строкой, числом, символом или массивом, существует множество встроенных функций для обнаружения этого. Также было бы лучше использовать эти функции для проверки входных данных.
is_string
$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');
is_numeric
$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');
And it is so much better to use those functions to check input data with mysql_real_escape_string
.
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.
SECURITY UPDATE: The previous str_replace
version allowed injections by adding {#} tokens into user data. This preg_replace_callback
version doesn't cause problems if the replacement contains these tokens.