用 PDO 和准备好的语句替换 mysql_* 函数

Posted

技术标签:

【中文标题】用 PDO 和准备好的语句替换 mysql_* 函数【英文标题】:Replacing mysql_* functions with PDO and prepared statements 【发布时间】:2011-12-25 01:27:31 【问题描述】:

我一直都是做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*');

在使用此功能时,我总是使用简单的方法在进行查询之前转义任何数据,无论是INSERTSELECTUPDATE 还是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、博客、谷歌搜索),但没有一个人提到这些。原因似乎很奇怪,因为仅仅转义用户输入确实不安全,至少可以说不是好的做法。是的,我知道您可以使用 Regex 来解决它,但我很确定这还不够吗?

据我了解,当变量由用户输入给出时,使用 PDO/prepared 语句是一种更安全的方式来存储和检索数据库中的数据。唯一的麻烦是,切换(尤其是在我非常坚持以前的编码方式/习惯之后)有点困难。

现在我知道要使用 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_querymysql_fetch_arraymysql_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_? 赋予什么价值?我完全搞错了吗?

更新

到目前为止,我收到的回复非常有帮助,非常感谢你们!每个人都有一个+1,因为我睁开了眼睛看到了一些不同的东西。很难选择最佳答案,但我认为上校 Shrapnel 应得的,因为几乎涵盖了所有内容,甚至使用我不知道的自定义库进入其他数组!

但是感谢你们所有人:)

【问题讨论】:

可能是“用户名”和“电子邮件”字段的长度?例如。如果用户名是 varchar(32),那么长度参数应该是 32。 【参考方案1】:

感谢您提出有趣的问题。给你:

它逃脱了危险的字符,

你的概念完全是错误的。 事实上“危险人物”是一个神话,没有。 而 mysql_real_escape_string 转义只是一个字符串分隔符。从这个定义你可以得出它的局限性——它只适用于字符串

但是,它仍然容易受到其他可能包含安全字符的攻击,但可能对显示数据或在某些情况下恶意修改或删除数据有害。

你在这里混合了所有东西。 说到数据库,

对于字符串,它不易受到攻击。只要您的字符串被引用和转义,它们就不能“恶意修改或删除数据”。* 对于其他数据类型数据 - 是的,它无用。但不是因为它有点“不安全”,而是因为使用不当。

关于显示数据,我认为是PDO相关问题中的offtopic,因为PDO也与显示数据无关。

转义用户输入

^^^ 另一个需要注意的错觉!

用户输入与转义完全无关。正如您可以从前一个定义中了解到的那样,您必须转义字符串,而不是任何“用户输入”。所以,再说一遍:

无论来源如何,您都有转义字符串 不管来源如何,转义其他类型的数据是没有用的。

明白了吗? 现在,我希望您了解转义的局限性以及“危险人物”的误解。

据我了解,使用 PDO/prepared statements 更安全

不是真的。 事实上,我们可以动态添加四个不同的查询部分:

一个字符串 一个数字 标识符 语法关键字。

所以,您可以看到转义只涉及一个问题。 (当然,如果您将数字视为字符串(将它们放在引号中),如果适用,您也可以将它们设为安全)

虽然准备好的陈述涵盖 - 呃 - 整个 2 个问题!很重要;-)

对于其他 2 个问题,请参阅我之前的回答,In php when submitting strings to the database should I take care of illegal characters using htmlspecialchars() or use a regular expression?

现在,函数名称不同,所以我的 mysql_query、mysql_fetch_array、mysql_num_rows 等不再起作用。

那又是一个,PHP用户的严重错觉,是天灾,大祸:

即使在使用旧的 mysql 驱动程序时,也不应该在他们的代码中使用裸 API 函数!必须将它们放在一些库函数中以供日常使用! (不是作为一种魔法仪式,只是为了使代码更短、更少重复、防错、更一致和可读)。

PDO 也是如此!

现在再次回答你的问题。

但是通过使用它们,这是否消除了使用 mysql_real_escape_string 之类的需要?

是的。

但我认为这大致是从数据库中获取用户应该做什么的想法

不是获取,而是向查询中添加任何数据

如果我没记错的话,你必须在 PDO:PARAM_STR 之后给出一个长度

你可以,但你不必这样做。

现在,这一切安全吗?

在数据库安全方面,这段代码没有弱点。这里没有什么可以保证的。

为了显示安全性 - 只需在此站点中搜索 XSS 关键字。

希望我能对此事有所了解。

顺便说一句,对于长插入,您可以使用我有一天写的函数Insert/update helper function using 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's scaring tales)

【讨论】:

谢谢 :) 不,我猜。我通常会标记问题本身,如果我想添加书签 当 OP 实际询问如何正确使用 PDO 时,为什么要鼓励 OP 使用自编码库?第一步先来,你知道吗?一个菜鸟(刚刚了解到不止 mysql_*)应该在考虑自定义 DB 包装库之前真正学习基本的 PDO 用法。此外,像您这样的两行代码是阅读/理解/调试/修改的 PITA。短代码并不总是最好的代码。 好吧,虽然总的来说这有点好,但我相信无论如何应该有一个警告,只是为了说明要走的路。而且,为了捍卫我的 2-liner,我敢说它比你的 7-liner 更易于调试,它可读且易于修改。如果您能向我展示此特定代码的某些弱点,而不是一般的 2 行代码,我将不胜感激。 @Col.Shrapnel - 你再次回复了一个非常详细和建设性的答案!每个人都有!无论如何,我肯定对这一切有更好的理解,我会尝试你的方法和 riha 的方法,看看我觉得舒服,直到我对这一切有了更多的理解。谢谢! @Col.Shrapnel 我发现代码更难阅读和编辑,但这可能是个人喜好。但是,它不能很好地与 git/mercurial 等 VCS 配合使用。想象一下,您必须在功能分支的 WHERE 条件中添加一个新行。然后在另一个分支上有另一个变化。现在两个功能分支都必须合并到主分支 - 合并冲突!在您编写代码几个月后享受解决它的乐趣,并且早就忘记了代码是关于什么的。考虑到这一点,在少数几行代码上一起咀嚼代码几乎总是毫无疑问的。当你最不期待它时,它会咬你一口。 :)【参考方案2】:

我从不关心 bindParam() 或参数类型或长度。

我只是将一组参数值传递给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) );

这同样有效,而且更容易编码。

您可能还对我的演示文稿SQL Injection Myths and Fallacies 或我的书SQL Antipatterns: Avoiding the Pitfalls of Database Programming 感兴趣。

【讨论】:

这个也可以吗?那么数组中的占位符呢?说SELECT * FROM users WHERE name = ? AND email = ?");execute ( array($name, $email) ); 还是在这种情况下不起作用? 是的,您可以使用位置参数而不是命名参数,就像您展示的那样。【参考方案3】:

是的,:something 是 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()

一个变量绑定到您的占位符,当查询为 executed 时将读取该变量,而不是在调用 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()。每个键都是查询中的一个命名占位符(请参阅 Bill Karwins 的回答)。数组的顺序并不重要。

附带说明:使用这种方法,您无法为 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.';

【讨论】:

那么 try/catch 块是否类似于 if/else 语句?感谢您指出不同的选项,这应该给我一些可以玩的东西! 另一个快速的问题,$e 将用于什么,我假设您引用它是因为错误? 不完全。如果在 try 块中的任何地方抛出异常,它将中止 try 块的执行并执行 catch 块。 $e 是抛出的异常。它包含错误消息/代码/文件/行等,可用于向管理员发送电子邮件或记录到文件,以便您调查出了什么问题。总结:try/catch 非常适合错误处理。真是个双关语哈哈。【参考方案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)。

【讨论】:

以上是关于用 PDO 和准备好的语句替换 mysql_* 函数的主要内容,如果未能解决你的问题,请参考以下文章

PDO - 使用准备好的语句[重复]

PDO 是不是仍在为 MySQL 模拟准备好的语句?

从 mysql_query 转换为准备好的语句 (mysqli/PDO)?必要的?

使用 PDO 准备好的语句 MySQL 错误

MySQL 全文连字符和大括号返回错误(最初:PDO 准备好的语句没有做它的工作?)

在 PHP 中到处使用准备好的语句? (PDO)