简体   繁体   English

用PDO和预处理语句替换mysql_ *函数

[英]Replacing mysql_* functions with PDO and prepared statements

I've always done the simple connection of mysql_connect , mysql_pconnect : 我总是完成mysql_connectmysql_pconnect的简单连接:

$db = mysql_pconnect('*host*', '*user*', '*pass*');

if (!$db) {
    echo("<strong>Error:</strong> Could not connect to the database!");
    exit;
}

mysql_select_db('*database*');

While using this I've always used the simple method to escape any data before making a query, whether that be INSERT , SELECT , UPDATE or DELETE by using mysql_real_escape_string 在使用这个时,我总是使用简单的方法在进行查询之前转义任何数据,无论是使用mysql_real_escape_string进行INSERTSELECTUPDATE还是DELETE

$name = $_POST['name'];

$name = mysql_real_escape_string($name);

$sql = mysql_query("SELECT * FROM `users` WHERE (`name` = '$name')") or die(mysql_error());

Now I understand this is safe, to an extent! 现在我明白这在某种程度上是安全的!

It escapes dangerous characters; 它逃脱了危险的人物; however, it is still vulnerable to other attacks which can contain safe characters but may be harmful to either displaying data or in some cases, modifying or deleting data maliciously. 但是,它仍然容易受到其他可能包含安全字符的攻击,但可能对显示数据或在某些情况下恶意修改或删除数据有害。

So, I searched a little bit and found out about PDO, MySQLi and prepared statements. 所以,我搜索了一下,发现了PDO,MySQLi和准备好的语句。 Yes, I may be late to the game but I've read many, many tutorials (tizag, W3C, blogs, Google searches) out there and not a single one has mentioned these. 是的,我可能会迟到,但我已经阅读了很多很多教程(tizag,W3C,博客,谷歌搜索),没有一个人提到这些。 It seems very strange as to why, as just escaping user input really isn't secure and not good practice to say the least. 看起来很奇怪为什么,因为只是逃避用户输入真的不安全而且至少可以说是不好的做法。 Yes, I'm aware you could use Regex to tackle it, but still, I'm pretty sure that's not enough? 是的,我知道你可以使用Regex解决它,但是,我很确定这还不够吗?

It is to my understanding that using PDO/prepared statements is a much safer way to store and retrieve data from a database when the variables are given by user input. 据我所知,当用户输入给出变量时,使用PDO /预处理语句是一种更安全的方式来存储和检索数据库中的数据。 The only trouble is, the switch over (especially after being very stuck in my ways/habits of previous coding) is a little difficult. 唯一的麻烦是,切换(特别是在我的方式/先前编码的习惯非常困难之后)有点困难。

Right now I understand that to connect to my database using PDO I would use 现在我明白使用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';
}

Now, function names are different so no longer will my mysql_query , mysql_fetch_array , mysql_num_rows etc work. 现在,函数名称不同,所以我的mysql_querymysql_fetch_arraymysql_num_rows等不再有效。 So I'm having to read/remember a load of new ones, but this is where I'm getting confused. 所以我必须阅读/记住一些新的,但这是我感到困惑的地方。

If I wanted to insert data from say a sign up/registration form, how would I go about doing this, but mainly how would I go about it securely? 如果我想从注册/注册表单中插入数据,我将如何进行此操作,但主要是如何安全地进行操作? I assume this is where prepared statements come in, but by using them does this eliminate the need to use something like mysql_real_escape_string ? 我假设这是准备语句的来源,但通过使用它们,这是否消除了使用mysql_real_escape_string类的需要? I know that mysql_real_escape_string requires you to be connected to a database via mysql_connect / mysql_pconnect so now we aren't using either won't this function just produce an error? 我知道mysql_real_escape_string要求你通过mysql_connect / mysql_pconnect连接到数据库所以现在我们不使用这个函数不会产生错误吗?

I've seen different ways to approach the PDO method too, for example, I've seen :variable and ? 我也看到了不同的方法来处理PDO方法,例如,我见过:variable? as what I think are known as place holders (sorry if that is wrong). 正如我所说的那样被称为占位符(对不起,如果这是错误的)。

But I think this is roughly the idea of what should be done to fetch a user from a database 但我认为这大致是为了从数据库中获取用户应该做些什么

$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);

But then I'm stuck on a couple things, if the variable wasn't a number and was a string of text, you have to given a length after PDO:PARAM_STR if I'm not mistaken. 但后来我坚持了几件事情,如果变量不是一个数字并且是一串文本,你必须在PDO:PARAM_STR之后给出一个长度PDO:PARAM_STR如果我没有弄错的话。 But how can you give a set length if you're not sure on the value given from user in-putted data, it can vary each time? 但是如果你不确定用户输入数据给出的值,你怎么能给出一个设定的长度,它每次都会变化? Either way, as far as I know to display the data you then do 无论哪种方式,据我所知,您可以显示数据

$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."");

Now, is this all safe? 现在,这一切都安全吗?

If I am right, would inserting data be the same for example: 如果我是对的,插入数据会是相同的,例如:

 $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();

Would that work, and is that safe too? 那会有用吗,那也安全吗? If it is right what value would I put in for the ?_LENGTH_? 如果它是正确的,我会为?_LENGTH_?输入什么价值?_LENGTH_? ? Have I got this all completely wrong? 我完全错了吗?

UPDATE UPDATE

The replies I've had so far have been extremely helpful, can't thank you guys enough! 到目前为止我的回复非常有帮助,不能谢谢你们! Everyone has got a +1 for opening my eyes up to something a little different. 每个人都有一个+1打开我的眼睛到一些有点不同的东西。 It's difficult to choose the top answer, but I think Col. Shrapnel deserves it as everything is pretty much covered, even going into other arrays with custom libraries which I wasn't aware of! 很难选择最佳答案,但我认为Col. Shrapnel应该得到它,因为一切都被覆盖,甚至进入其他阵列,我不知道自定义库!

But thanks to all of you:) 但多亏你们所有人:)

Thanks for the interesting question. 感谢有趣的问题。 Here you go: 干得好:

It escapes dangerous characters, 它摆脱了危险的角色,

Your concept is utterly wrong. 你的概念是完全错误的。
In fact "dangerous characters" is a myth, there are none. 事实上“危险人物”是一个神话,没有。 And mysql_real_escape_string escaping but merely a string delimiters . 并且mysql_real_escape_string转义但仅仅是一个字符串分隔符 From this definition you can conclude it's limitations - it works only for strings . 从这个定义中你可以得出结论它的局限性 - 它只适用于字符串

however, it is still vulnerable to other attacks which can contain safe characters but may be harmful to either displaying data or in some cases, modifying or deleting data maliciously. 但是,它仍然容易受到其他可能包含安全字符的攻击,但可能对显示数据或在某些情况下恶意修改或删除数据有害。

You're mixing here everything. 你在这里混合一切。
Speaking of database, 说到数据库,

  • for the strings it is NOT vulnerable. 对于字符串,它不容易受到攻击。 As long as your strings being quoted and escaped, they cannot "modify or delete data maliciously". 只要您的字符串被引用和转义,它们就无法 “恶意修改或删除数据”。 *
  • for the other data typedata - yes, it's useless . 对于其他数据类型数据 - 是的,它没用 But not because it is somewhat "unsafe" but just because of improper use. 但不是因为它有点“不安全”,而是因为使用不当。

As for the displaying data, I suppose it is offtopic in the PDO related question, as PDO has nothing to do with displaying data either. 至于显示数据,我认为它在PDO相关问题中是offtopic ,因为PDO也与显示数据无关。

escaping user input 逃避用户输入

^^^ Another delusion to be noted! ^^^另一个需要注意的错觉!

  • a user input has absolutely nothing to do with escaping . 用户输入与转义完全无关 As you can learn from the former definition, you have to escape strings, not whatever "user input". 正如您可以从前一个定义中学习的那样,您必须转义字符串,而不是“用户输入”。 So, again: 所以,再次:

    • you have escape strings, no matter of their source 你有逃脱字符串,无论其来源如何
    • it is useless to escape other types of data, no matter of the source. 无论来源如何,逃避其他类型的数据都是无用的。

Got the point? 明白了吗?
Now, I hope you understand the limitations of escaping as well as the "dangerous characters" misconception. 现在,我希望你了解逃避的局限性以及“危险人物”的误解。

It is to my understanding that using PDO/prepared statements is a much safer 据我所知,使用PDO /预处理语句更安全

Not really. 并不是的。
In fact, there are four different query parts which we can add to it dynamically: 实际上,我们可以动态添加四个不同的查询部分:

  • a string 一个字符串
  • a number 一个号码
  • an identifier 标识符
  • a syntax keyword. 语法关键字。

so, you can see that escaping covers only one issue. 所以,你可以看到转义仅涵盖一个问题。 (but of course, if you treat numbers as strings (putting them in quotes), when applicable , you can make them safe as well) (但是,当然,如果您将数字视为字符串(将它们放在引号中), 在适用的情况下 ,您也可以使它们安全)

while prepared statements cover - ugh - whole 2 isues! 准备好的陈述涵盖 - 呃 - 整个2个等级! A big deal ;-) 很重要;-)

For the other 2 issues see my earlier answer, In PHP when submitting strings to the database should I take care of illegal characters using htmlspecialchars() or use a regular expression? 对于其他2个问题,请参阅我之前的回答, 在PHP中,当我向数据库提交字符串时,我应该使用htmlspecialchars()处理非法字符还是使用正则表达式?

Now, function names are different so no longer will my mysql_query, mysql_fetch_array, mysql_num_rows etc work. 现在,函数名称不同,所以我的mysql_query,mysql_fetch_array,mysql_num_rows等不再有效。

That is another, grave delusion of PHP users , a natural disaster, a catastrophe: 这是PHP 用户的另一个严重妄想,一场自然灾害,一场大灾难:

Even when utilizing old mysql driver, one should never use bare API functions in their code! 即使使用旧的mysql驱动程序, 也不应该在代码中使用裸API函数 One have to put them in some library function for the everyday usage! 一个人必须把它们放在一些库功能中以供日常使用! (Not as a some magic rite but just to make the code shorter, less repetitive, error-proof, more consistent and readable). (不仅仅是为了使代码更短,更少重复,防错,更一致和可读)。

The same goes for the PDO as well! PDO也是如此!

Now on with your question again. 现在再次提出你的问题。

but by using them does this eliminate the need to use something like mysql_real_escape_string? 但通过使用它们,这是否消除了使用mysql_real_escape_string之类的需要?

YES. 是。

But I think this is roughly the idea of what should be done to fetch a user from a database 但我认为这大致是为了从数据库中获取用户应该做些什么

Not to fetch, but to add a whatever data to the query ! 不是要获取,而是向查询添加任何数据

you have to given a length after PDO:PARAM_STR if I'm not mistaken 你必须在PDO之后给出一个长度:PARAM_STR如果我没有弄错的话

You can, but you don't have to. 你可以,但你不必。

Now, is this all safe? 现在,这一切都安全吗?

In terms of database safety there are just no weak spots in this code. 就数据库安全而言,此代码中没有任何弱点。 Nothing to secure here. 没有什么可以保证在这里。

for the displaying security - just search this site for the XSS keyword. 显示安全性 - 只需在此站点中搜索XSS关键字。

Hope I shed some light on the matter. 希望我对此事有所了解。

BTW, for the long inserts you can make some use of the function I wrote someday, Insert/update helper function using PDO 顺便说一句,对于长插入,你可以使用我有一天写的函数,使用PDO插入/更新辅助函数

However, I am not using prepared statements at the moment, as I prefer my home-brewed placeholders over them, utilizing a library I mentioned above. 但是,我现在没有使用预先准备好的陈述,因为我更喜欢使用我上面提到的库 ,而不是我自己的家庭占位符。 So, to counter the code posted by the riha below, it would be as short as these 2 lines: 因此,为了对抗下面的riha发布的代码,它将与这两行一样短:

$sql  = 'SELECT * FROM `users` WHERE `name`=?s AND `type`=?s AND `active`=?i';
$data = $db->getRow($sql,$_GET['name'],'admin',1);

But of course you can have the same code using prepared statements as well. 但是,当然您也可以使用预准备语句使用相同的代码。


* (yes I am aware of the Schiflett's scaring tales)

I never bother with bindParam() or param types or lengths. 我从不打扰bindParam()或param类型或长度。

I just pass an array of parameter values to execute(), like this: 我只是将一个参数值数组传递给execute(),如下所示:

$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) );

This is just as effective, and easier to code. 这同样有效,也更容易编码。

You may also be interested in my presentation SQL Injection Myths and Fallacies , or my book SQL Antipatterns: Avoiding the Pitfalls of Database Programming . 您可能也对我的演示文稿SQL注入神话和谬误 ,或我的书SQL反模式:避免数据库编程的陷阱感兴趣。

Yes, :something is a named placeholder in PDO, ? 是的,:某些东西是PDO中的命名占位符,? is an anonymous placeholder. 是一个匿名占位符。 They allow you to either bind values one by one or all at once. 它们允许您逐个绑定值或一次绑定所有值。

So, basically that makes four options to provide your query with values. 所以,基本上这有四个选项来为您的查询提供值。

One by one with bindValue() 使用bindValue()一个接一个

This binds a concrete value to your placeholder as soon as you call it. 一旦调用它,就会将具体值绑定到占位符。 You may even bind hard coded strings like bindValue(':something', 'foo') if desired. 如果需要,你甚至可以绑定像bindValue(':something', 'foo')这样的硬编码字符串。

Providing a parameter type is optional (but suggested). 提供参数类型是可选的(但建议)。 However, since the default is PDO::PARAM_STR , you only need to specify it when it is not a string. 但是,由于默认值为PDO::PARAM_STR ,因此只需在不是字符串时指定它。 Also, PDO will take care of the length here - there is no length parameter. 此外, 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();
...

I usually prefer this approach. 我通常更喜欢这种方法。 I find it the cleanest and most flexible. 我发现它是最干净,最灵活的。

One by one with bindParam() 使用bindParam()一个接一个

A variable is bound to your placeholder that will be read when the query is executed , NOT when bindParam() is called. 变量绑定到占位符,在执行查询时将读取,而不是在调用bindParam()时。 That may or may not be what you want. 这可能是也可能不是你想要的。 It comes in handy when you want to repeatedly execute your query with different values. 当您想要使用不同的值重复执行查询时,它会派上用场。

$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();
  ...
}

You only prepare and bind once which safes CPU cycles. 您只准备和绑定一次保护CPU周期。 :) :)

All at once with named placeholders 同时使用命名占位符

You just drop in an array to execute() . 你只需要插入一个数组来execute() Each key is a named placeholder in your query (see Bill Karwins answer). 每个键都是查询中的命名占位符(请参阅Bill Karwins的回答)。 The order of the array is not important. 数组的顺序并不重要。

On a side note: With this approach you cannot provide PDO with data type hints (PDO::PARAM_INT etc.). 旁注:使用此方法,您无法为PDO提供数据类型提示(PDO :: PARAM_INT等)。 AFAIK, PDO tries to guess. AFAIK,PDO试图猜测。

All at once with anonymous placeholders 一次性使用匿名占位符

You also drop in an array to execute(), but it is numerically indexed (has no string keys). 您还将一个数组放入execute(),但它是用数字索引的(没有字符串键)。 The values will replace your anonymous placeholders one by one in the order they appear in your query/array - first array value replaces first placeholder and so forth. 这些值将按照它们在查询/数组中出现的顺序逐个替换您的匿名占位符 - 第一个数组值替换第一个占位符,依此类推。 See erm410's answer. 请参阅erm410的回答。

As with the array and named placeholders, you cannot provide data type hints. 与数组和命名占位符一样,您无法提供数据类型提示。

What they have in common 他们有什么共同点

  • All of those require you to bind/provide as much values as you have placeholders. 所有这些都要求您绑定/提供与占位符一样多的值。 If you bind too many/few, PDO will eat your children. 如果你绑定太少/很少,PDO会吃掉你的孩子。
  • You don't have to take care about escaping, PDO handles that. 您不必关心转义,PDO处理它。 Prepared PDO statements are SQL injection safe by design. 准备好的PDO语句是SQL注入安全设计。 However, that's not true for exec() and query() - you should generally only use those two for hardcoded queries. 但是,对于exec()query() ,情况并非如此 - 通常只应将这两个用于硬编码查询。

Also be aware that PDO throws exceptions . 另请注意, PDO会抛出异常 Those could reveal potentially sensitive information to the user. 这些可以向用户揭示潜在的敏感信息。 You should at least put your initial PDO setup in a try/catch block ! 您至少应该将初始PDO设置放在try / catch块中

If you don't want it to throw Exceptions later on, you can set the error mode to warning. 如果您不希望它稍后抛出异常,则可以将错误模式设置为警告。

try {
  $db = new PDO(...);
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING)
} catch (PDOException $e) {
  echo 'Oops, something went wrong with the database connection.';
}

To answer the length question, specifying it is optional unless the param you are binding is an OUT parameter from a stored procedure, so in most cases you can safely omit it. 要回答长度问题,指定它是可选的,除非您绑定的参数是存储过程的OUT参数,因此在大多数情况下您可以安全地省略它。

As far as safety goes, escaping is done behind the scenes when you bind the parameters. 就安全性而言,绑定参数时,在后台进行转义。 This is possible because you had to create a database connection when you created the object. 这是可能的,因为您必须在创建对象时创建数据库连接。 You are also protected from SQL injection attacks since by preparing the statement, you are telling your database the format of the statement before user input can get anywhere near to it. 您也可以免受SQL注入攻击,因为通过准备语句,您可以告诉数据库在用户输入可以接近它之前的语句格式。 An example: 一个例子:

$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. */

Thus, in terms of safety, your examples above seem fine. 因此,就安全性而言,上面的例子似乎很好。

Finally, I agree that binding parameters individually is tedious and is just as effectively done with an array passed to PDOStatement->execute() (see http://www.php.net/manual/en/pdostatement.execute.php ). 最后,我同意单独绑定参数是繁琐的,并且与传递给PDOStatement-> execute()的数组一样有效(参见http://www.php.net/manual/en/pdostatement.execute.php )。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM