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

时间:2011-11-09 06:28:03

标签: php mysql database pdo prepared-statement

我总是完成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 /预处理语句是一种更安全的方法来存储和检索数据库中的数据。唯一的麻烦是,切换(特别是在我的方式/先前编码的习惯非常困难之后)有点困难。

现在我明白要使用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应该得到它,因为一切都被覆盖,甚至进入其他我不知道的自定义库阵列!

但是感谢你们所有人:)

4 个答案:

答案 0 :(得分:12)

感谢有趣的问题。你走了:

  

它逃脱了危险的角色,

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

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

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

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

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

  

转发用户输入

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

  • 用户输入与转义完全无关。正如您可以从前一个定义中学习的那样,您必须转义字符串,而不是“用户输入”。所以,再次:

    • 你有逃脱字符串,无论其来源如何
    • 无论来源如何,都无法逃避其他类型的数据。

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

  

据我了解,使用PDO /预处理语句更安全

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

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

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

准备好的陈述涵盖了 - 呃 - 整个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之类的东西吗?

YES。

  

但我认为这大致是为了从数据库中获取用户应采取的措施

不提取,但向查询添加任何数据

  

你必须在PDO之后给出一个长度:PARAM_STR如果我没有弄错

你可以,但你不必。

  

现在,这一切都安全吗?

就数据库安全而言,此代码中没有任何弱点。没有什么可以保证在这里。

显示安全性 - 只需在此网站上搜索XSS关键字。

希望我对此事有所了解。

BTW,对于长插入,您可以使用我有一天写的函数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)

答案 1 :(得分:8)

我从不打扰bindParam()或param类型或长度。

我只是将一个参数值数组传递给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感兴趣。

答案 2 :(得分:5)

是的,:某些东西是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试图猜测。

同时使用匿名占位符

你也可以将一个数组放入execute(),但它是用数字索引的(没有字符串键)。这些值将按照它们在查询/数组中出现的顺序逐个替换您的匿名占位符 - 第一个数组值替换第一个占位符,依此类推。请参阅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.';
}

答案 3 :(得分: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-&gt; execute()的数组一样有效(参见http://www.php.net/manual/en/pdostatement.execute.php)。