用 PDO 和准备好的语句替换 mysql_* 函数
Posted
技术标签:
【中文标题】用 PDO 和准备好的语句替换 mysql_* 函数【英文标题】:Replacing mysql_* functions with PDO and prepared statements 【发布时间】:2011-12-25 01:27:31 【问题描述】:我一直都是做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、博客、谷歌搜索),但没有一个人提到这些。原因似乎很奇怪,因为仅仅转义用户输入确实不安全,至少可以说不是好的做法。是的,我知道您可以使用 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_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_?
赋予什么价值?我完全搞错了吗?
更新
到目前为止,我收到的回复非常有帮助,非常感谢你们!每个人都有一个+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_* 函数的主要内容,如果未能解决你的问题,请参考以下文章
从 mysql_query 转换为准备好的语句 (mysqli/PDO)?必要的?