Замена функций mysql_ * с помощью PDO и подготовленных операторов
Я всегда делал простую связь mysql_connect
, mysql_pconnect
:
$db = mysql_pconnect('*host*', '*user*', '*pass*');
if (!$db) {
echo("<strong>Error:</strong> Could not connect to the database!");
exit;
}
mysql_select_db('*database*');
При использовании этого я всегда использовал простой метод для удаления любых данных перед выполнением запроса, будь то INSERT
, SELECT
, UPDATE
или DELETE
с помощью mysql_real_escape_string
$name = $_POST['name'];
$name = mysql_real_escape_string($name);
$sql = mysql_query("SELECT * FROM `users` WHERE (`name` = '$name')") or die(mysql_error());
Теперь я понимаю, что это безопасно, в какой-то степени!
Он избегает опасных символов; тем не менее, он по-прежнему уязвим для других атак, которые могут содержать безопасные символы, но могут быть вредны для отображения данных или в некоторых случаях, изменения или удаления данных злонамеренно.
Итак, я немного искал и узнал о PDO, MySQLi и подготовленных операциях. Да, я могу опаздывать на игру, но я читал много, много учебников (tizag, W3C, блоги, поисковые запросы Google), и ни один из них не упомянул об этом. Похоже, очень странно, почему, так как просто избежать пользовательского ввода действительно небезопасно, а не хорошая практика, если не сказать больше. Да, я знаю, что вы можете использовать Regex, чтобы справиться с этим, но все же, я уверен, что этого недостаточно?
Насколько я понимаю, использование PDO/подготовленных операторов - гораздо более безопасный способ хранения и извлечения данных из базы данных, когда переменные задаются пользователем. Единственная проблема заключается в том, что переход (особенно после того, как я сильно застрял в моих привычках/привычках предыдущего кодирования) немного сложнее.
Сейчас я понимаю, что для подключения к моей базе данных с использованием PDO я бы использовал
$hostname = '*host*';
$username = '*user*';
$password = '*pass*';
$database = '*database*'
$dbh = new PDO("mysql:host=$hostname;dbname=$database", $username, $password);
if ($dbh) {
echo 'Connected to database';
} else {
echo 'Could not connect to database';
}
Теперь имена функций разные, поэтому больше не будут работать мои mysql_query
, mysql_fetch_array
, mysql_num_rows
и т.д. Поэтому мне приходится читать/запоминать загрузку новых, но здесь я смущаюсь.
Если бы я хотел вставить данные из формы регистрации/регистрации, как бы я это сделал, но в основном, как бы я мог сделать это безопасно? Я предполагаю, что это то, где приходят подготовленные заявления, но, используя их, это устраняет необходимость использовать что-то вроде mysql_real_escape_string
? Я знаю, что mysql_real_escape_string
требует, чтобы вы подключались к базе данных с помощью mysql_connect
/mysql_pconnect
, так что теперь мы не используем, не будет ли эта функция выдавать ошибку?
Я видел различные способы подхода к методу PDO, например, я видел :variable
и ?
как то, что, как я думаю, называется владельцами мест (извините, если это не так).
Но я думаю, что это примерно идея о том, что нужно сделать для извлечения пользователя из базы данных
$user_id = $_GET['id']; // For example from a URL query string
$stmt = $dbh->prepare("SELECT * FROM `users` WHERE `id` = :user_id");
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
Но тогда я застрял на нескольких вещах, если переменная не была числом и была строкой текста, вам нужно дать длину после PDO:PARAM_STR
, если я не ошибаюсь. Но как вы можете дать заданную длину, если вы не уверены в значении, указанном в пользовательских данных, он может меняться каждый раз? В любом случае, насколько я знаю, чтобы отобразить данные, которые вы затем выполняете
$stmt->execute();
$result = $stmt->fetchAll();
// Either
foreach($result as $row) {
echo $row['user_id'].'<br />';
echo $row['user_name'].'<br />';
echo $row['user_email'];
}
// Or
foreach($result as $row) {
$user_id = $row['user_id'];
$user_name = $row['user_name'];
$user_email = $row['user_email'];
}
echo("".$user_id."<br />".$user_name."<br />".$user_email."");
Теперь, это все безопасно?
Если я прав, вставка данных будет одинаковой, например:
$username = $_POST['username'];
$email = $_POST['email'];
$stmt = $dbh->prepare("INSERT INTO `users` (username, email)
VALUES (:username, :email)");
$stmt->bindParam(':username, $username, PDO::PARAM_STR, ?_LENGTH_?);
$stmt->bindParam(':email, $email, PDO::PARAM_STR, ?_LENGTH_?);
$stmt->execute();
Будет ли это работать, и это тоже безопасно? Если правильно, какое значение я бы добавил для ?_LENGTH_?
? У меня все это было совершенно неправильно?
UPDATE
Ответы, которые у меня были до сих пор, были чрезвычайно полезны, не могу поблагодарить вас, ребята, достаточно! У каждого есть +1 для открытия моих глаз до чего-то другого. Трудно выбрать верный ответ, но я думаю, что Col. Shrapnel заслуживает этого, поскольку все в значительной степени покрыто, даже если он входит в другие массивы с пользовательскими библиотеками, о которых я не знал!
Но спасибо всем вам:)
Ответы
Ответ 1
Спасибо за интересный вопрос. Вот вы:
Он избегает опасных символов,
Ваша концепция полностью неверна.
На самом деле "опасные персонажи" - это миф, их нет.
А mysql_real_escape_string - экранирование, а просто ограничители строк. Из этого определения вы можете заключить его ограничения - он работает только для строк.
однако он по-прежнему уязвим для других атак, которые могут содержать безопасные символы, но могут быть вредны для отображения данных или в некоторых случаях, изменения или удаления данных злонамеренно.
Вы все здесь смешиваете.
Говоря о базе данных,
- для строк НЕ уязвим. Пока ваши строки цитируются и экранируются, они не могут "злонамеренно изменять или удалять данные".
*
- для других данных typedata - да, это бесполезно. Но не потому, что это несколько "небезопасно", а только из-за неправильного использования.
Что касается отображения данных, я полагаю, что это проблема offtopic в вопросе, связанном с PDO, поскольку PDO не имеет ничего общего с отображением данных.
выход из пользовательского ввода
^^^ Еще одно заблуждение, которое нужно отметить!
-
Пользовательский ввод не имеет ничего общего с экранированием. Как вы можете узнать из предыдущего определения, вам нужно избегать строк, а не "пользовательский ввод". Итак, снова:
- у вас есть escape строки, независимо от их источника
- бесполезно избегать других типов данных независимо от источника.
Получена точка?
Теперь я надеюсь, что вы понимаете ограничения выхода, а также ошибочное представление "опасных персонажей".
Насколько я понимаю, использование PDO/подготовленных операторов намного безопаснее
Не очень.
На самом деле существует четыре разных частей запроса, которые мы можем добавить к нему динамически:
- строка
- число
- идентификатор
- ключевое слово синтаксиса.
Итак, вы можете видеть, что экранирование охватывает только одну проблему. (но, конечно, если вы обрабатываете числа как строки (помещая их в кавычки), когда это применимо, вы также можете сделать их безопасными)
в то время как подготовленные заявления охватывают - ugh - целые 2 номера! Большое дело; -)
Для остальных 2 вопросов см. мой предыдущий ответ В PHP при отправке строк в базу данных следует позаботиться о недопустимых символах с помощью htmlspecialchars() или использовать регулярное выражение?
Теперь имена функций разные, поэтому больше не работают мои mysql_query, mysql_fetch_array, mysql_num_rows и т.д.
Это другое, серьезное заблуждение пользователей PHP, стихийное бедствие, катастрофа:
Даже при использовании старого драйвера mysql никогда не следует использовать в своем коде полезные функции API! Нужно поместить их в библиотечную функцию для повседневного использования! (Не как какой-то волшебный обряд, а просто для того, чтобы сделать код короче, менее повторяющимся, безошибочным, более последовательным и читаемым).
То же самое касается и PDO!
Теперь повторю вопрос.
но, используя их, это устраняет необходимость использовать что-то вроде mysql_real_escape_string?
ДА.
Но я думаю, что это примерно идея о том, что нужно сделать для извлечения пользователя из базы данных
Не извлекать, а добавлять какие-либо данные в запрос!
вам нужно указать длину после PDO: PARAM_STR, если я не ошибаюсь
Вы можете, но вам этого не нужно.
Теперь, это все безопасно?
В плане безопасности базы данных в этом коде отсутствуют только слабые места. Здесь ничего нет.
для отображения безопасности - просто выполните поиск этого сайта для ключевого слова XSS
.
Надеюсь, что я пролил свет на этот вопрос.
BTW, для длинных вставок вы можете использовать некоторую функцию, которую я написал когда-нибудь, Вставить/обновить вспомогательную функцию с помощью PDO
Тем не менее, я не использую подготовленные заявления на данный момент, поскольку я предпочитаю, чтобы мои домашние заготовки над ними, используя библиотеку, о которой я упоминал выше. Таким образом, чтобы противостоять коду, опубликованному ниже riha, он будет короче, чем эти две строки:
$sql = 'SELECT * FROM `users` WHERE `name`=?s AND `type`=?s AND `active`=?i';
$data = $db->getRow($sql,$_GET['name'],'admin',1);
Но, конечно, вы можете иметь тот же код и с помощью подготовленных операторов.
* (yes I am aware of the Schiflett scaring tales)
Ответ 2
Я никогда не беспокоюсь о bindParam() или параметрах или длинах.
Я просто передаю массив значений параметров для выполнения(), например:
$stmt = $dbh->prepare("SELECT * FROM `users` WHERE `id` = :user_id");
$stmt->execute( array(':user_id' => $user_id) );
$stmt = $dbh->prepare("INSERT INTO `users` (username, email)
VALUES (:username, :email)");
$stmt->execute( array(':username'=>$username, ':email'=>$email) );
Это так же эффективно и проще кодировать.
Вы также можете быть заинтересованы в моей презентации SQL Injection Myths and Fallacies, или моей книге SQL Antipatterns: избежание ошибок программирования базы данных.
Ответ 3
Да, что-то является именованным заполнителем в PDO,? является анонимным заполнителем. Они позволяют либо привязывать значения один за другим, либо все сразу.
Таким образом, в основном это дает четыре параметра, чтобы предоставить ваш запрос значения.
Один за другим с bindValue()
Это связывает конкретное значение с вашим заполнителем, как только вы его вызываете. Вы можете даже привязать жестко закодированные строки, например bindValue(':something', 'foo')
, если хотите.
Предоставление типа параметра необязательно (но предлагается). Однако, поскольку по умолчанию это PDO::PARAM_STR
, вам нужно только указать его, когда это не строка. Кроме того, PDO
позаботится о длине здесь - нет параметра длины.
$sql = '
SELECT *
FROM `users`
WHERE
`name` LIKE :name
AND `type` = :type
AND `active` = :active
';
$stm = $db->prepare($sql);
$stm->bindValue(':name', $_GET['name']); // PDO::PARAM_STR is the default and can be omitted.
$stm->bindValue(':type', 'admin'); // This is not possible with bindParam().
$stm->bindValue(':active', 1, PDO::PARAM_INT);
$stm->execute();
...
Обычно я предпочитаю этот подход. Я считаю его самым чистым и гибким.
Один за другим с bindParam()
Переменная привязана к вашему заполнителю, который будет читаться, когда запрос выполнен, а не когда bindParam ( ) называется. Это может быть или не быть тем, что вы хотите. Это пригодится, если вы хотите многократно выполнять запрос с разными значениями.
$sql = 'SELECT * FROM `users` WHERE `id` = :id';
$stm = $db->prepare($sql);
$id = 0;
$stm->bindParam(':id', $id, PDO::PARAM_INT);
$userids = array(2, 7, 8, 9, 10);
foreach ($userids as $userid) {
$id = $userid;
$stm->execute();
...
}
Вы только подготовляете и связываете один раз, который защищает циклы CPU.:)
Все сразу с именованными заполнителями
Вы просто добавляете массив в execute()
. Каждый ключ является именованным заполнителем в вашем запросе (см. Ответ Билла Карвинса). Порядок массива не имеет значения.
С одной стороны: при таком подходе вы не можете предоставить PDO с подсказками типа данных (PDO:: PARAM_INT и т.д.). AFAIK, PDO пытается угадать.
Все сразу с анонимными заполнителями
Вы также забрасываете массив для выполнения(), но его числовое индексирование (без строковых ключей). Значения заменят ваши анонимные заполнители один за другим в порядке их появления в вашем запросе/массиве - первое значение массива заменяет первый заполнитель и так далее. См. Ответ erm410.
Как и в случае с массивом и названными заполнителями, вы не можете указывать типы данных.
Что у них общего
- Все из них требуют, чтобы вы связывали/предоставляли столько же значений, сколько у вас есть.
заполнители. Если вы связываете слишком много/несколько, PDO будет есть ваших детей.
- Вам не нужно заботиться об экранировании, PDO обрабатывает это. Подготовленные PDO-заявления являются безопасными SQL-инъекциями по дизайну. Однако это не верно для exec() и query() - вы должны использовать только эти два для жестко запрограммированных запросов.
Также имейте в виду, что PDO выдает исключения. Они могут выявить потенциально чувствительную информацию для пользователя. Вы должны по крайней мере поставить свою начальную настройку PDO в блок try/catch!
Если вы не хотите, чтобы он позже выдавал исключения, вы можете установить предупреждение об ошибке.
try {
$db = new PDO(...);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING)
} catch (PDOException $e) {
echo 'Oops, something went wrong with the database connection.';
}
Ответ 4
Чтобы ответить на вопрос о длине, указание его необязательно, если параметр, который вы связываете, является параметром OUT из хранимой процедуры, поэтому в большинстве случаев вы можете спокойно опустить его.
Что касается безопасности, то при связывании параметров выполняется экранирование за кулисами. Это возможно, потому что вам нужно было создать соединение с базой данных при создании объекта. Вы также защищены от атак SQL-инъекций, поскольку, подготовив инструкцию, вы сообщаете своей базе данных формат инструкции до того, как пользовательский вход может приблизиться к ней. Пример:
$id = '1; MALICIOUS second STATEMENT';
mysql_query("SELECT * FROM `users` WHERE `id` = $id"); /* selects user with id 1
and the executes the
malicious second statement */
$stmt = $pdo->prepare("SELECT * FROM `users` WHERE `id` = ?") /* Tells DB to expect a
single statement with
a single parameter */
$stmt->execute(array($id)); /* selects user with id '1; MALICIOUS second
STATEMENT' i.e. returns empty set. */
Таким образом, с точки зрения безопасности ваши приведенные выше примеры кажутся прекрасными.
Наконец, я согласен, что параметры привязки индивидуально утомительны и так же эффективно выполняются с массивом, переданным в PDOStatement- > execute() (см. http://www.php.net/manual/en/pdostatement.execute.php).