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

10

我一直使用简单的连接方式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、博客、Google搜索),没有一个提到过它们。这似乎非常奇怪,因为仅仅转义用户输入并不安全,至少不是好的实践。是的,我知道你可以使用正则表达式来解决它,但我确信那还不够?

据我了解,当变量由用户输入时,使用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_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,因为你们让我看到了一些不同的东西。很难选择最佳答案,但我认为Col. Shrapnel应该得到它,因为几乎所有东西都被涵盖了,甚至还涉及到使用自定义库的其他数组,这是我不知道的!

但感谢你们所有人:)


也许是'用户名'和'电子邮件'字段的长度?例如,如果用户名是varchar(32),那么长度参数应该是32。 - deejayy
4个回答

12

感谢提供有趣的问题。以下是回答:

它转义了危险字符,

你的概念完全错误
实际上,“危险字符”是一个谬论,根本不存在。mysql_real_escape_string只不过是转义字符串定界符,从这个定义中可以得出它的限制——它只适用于字符串

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

你在这里混淆了一切。
就数据库而言,

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

至于显示数据,我想这与PDO相关的问题是离题的,因为PDO与显示数据无关

转义用户输入

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

  • 用户输入与转义没有任何关系。从前面的定义可以看出,您必须转义字符串,而不是任何“用户输入”。所以,再次强调:

    • 您必须转义字符串,无论其来源如何
    • 转义其他类型的数据都是无用的,无论其来源如何。

明白了吗?
现在,我希望您了解转义的限制以及“危险字符”误解。

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

并不完全正确。
实际上,我们可以动态添加四个不同的查询部分:

  • 字符串
  • 数字
  • 标识符
  • 语法关键字。

因此,您可以看到转义仅涵盖一个问题。(但是当适用时,如果将数字视为字符串(将它们放在引号中),也可以使它们安全)

而预处理语句则涵盖了两个问题!非常重要;-)

有关另外两个问题,请参见我的早期回答,在PHP中提交字符串到数据库时,我应该使用htmlspecialchars()处理非法字符还是使用正则表达式?

现在,函数名称已经不同,因此我使用的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);

当然,您可以使用预处理语句编写相同的代码。

*(是的,我知道Schiflett的可怕故事)


为什么要鼓励OP使用自编库,而实际上OP正在询问如何正确使用PDO?先做第一步,你知道吗?一个新手(刚学会mysql_*之外的东西)应该在考虑自定义DB包装库之前真正学习基本的PDO用法。此外,像你这样的两行代码很难阅读/理解/调试/修改。短代码并不总是最好的代码。 - riha
虽然总的来说这是一个不错的观点,但我确信仍应该加上警告,以指明正确的方法。而且,为了维护我的两行代码,我敢说它比你的七行代码更易于调试,易读和易修改。如果你能向我展示这个特定代码的一些弱点(而不是一般性的两行代码),我会很感激的。 - Your Common Sense
@Col.Shrapnel - 再次感谢您提供了非常详细和有建设性的答案!每个人都很棒!我对这一切肯定有了更好的理解,我会尝试您和riha的方法,并看看哪种方法更适合我,直到我对所有内容有更深入的理解。谢谢! - no.
@Col.Shrapnel 我觉得这种代码更难阅读和编辑,但这可能是个人偏好。然而,它与像git/mercurial这样的版本控制系统不兼容。想象一下,你必须在一个功能分支上添加一个新的WHERE条件行。然后另一个分支上有了另一个变化。现在两个功能分支都必须合并到主分支上 - 合并冲突!在几个月后解决它时,你已经忘记了代码的含义。考虑到这一点,将代码拼凑在几行中几乎总是不可行的。当你最不希望它发生时,它会反咬你一口。 :) - riha
@riha,你是在谈论查询本身吗?无论你想怎么做,我的代码的重点是完全不同的。很抱歉你没有理解它。 - Your Common Sense
显示剩余3条评论

8

我从不烦恼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注入神话和谬误,或者我的书SQL反模式卷1:避免数据库编程的陷阱

这个可以用数组中的 ? 占位符来完成吗?例如 SELECT * FROM users WHERE name = ? AND email = ?"); 然后用 execute ( array($name, $email) ); 来执行,这样行得通吗? - no.
是的,您可以像您展示的那样使用位置参数而不是命名参数。 - Bill Karwin

5

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

一个变量绑定到你的占位符,当查询执行时将读取该变量,而不是在调用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等)。据我所知,PDO会尝试猜测。 使用匿名占位符一次性完成 你也可以将一个数组放入execute()中,但它是按数字索引的(没有字符串键)。这些值将依次替换你查询/数组中的匿名占位符 - 第一个数组值将替换第一个占位符,依此类推。请参见erm410的答案。
与数组和命名占位符一样,您无法提供数据类型提示。 它们共同的特点
  • 所有这些都需要你绑定/提供和占位符一样多的值。如果你绑定了太多/太少,PDO会吃掉你的孩子。
  • 你不必担心转义,PDO会处理它。准备好的PDO语句在设计上是防止SQL注入的。然而,exec()query()不适用于硬编码查询 - 通常只应使用这两个方法。

还要注意PDO会抛出异常。这可能会向用户透露潜在敏感信息。你应该至少将PDO初始设置放在try/catch块中

如果你不想让它在以后抛出异常,可以将错误模式设置为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.';
}

那么try/catch块类似于if/else语句吗?感谢您指出不同的选项,这应该让我有些东西可以玩耍! - no.
另一个快速的问题,$e 会用来做什么?我猜你是在引用它表示一个错误? - no.
不太对。如果try块中的任何地方抛出异常,则会中止try块的执行并执行catch块。 $ e是抛出的异常。它包含错误消息/代码/文件/行等,并可用于向管理员发送电子邮件或记录到文件中,以便您可以调查出了什么问题。总结:try / catch非常适用于错误处理。真是一个双关语哈哈。 - riha

2
回答长度问题,除非您绑定的参数是存储过程的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)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接