繁体   English   中英

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

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

我总是完成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*');

在使用这个时,我总是使用简单的方法在进行查询之前转义任何数据,无论是使用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());

现在我明白这在某种程度上是安全的!

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

所以,我搜索了一下,发现了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之后给出一个长度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_?输入什么价值?_LENGTH_? 我完全错了吗?

UPDATE

到目前为止我的回复非常有帮助,不能谢谢你们! 每个人都有一个+1打开我的眼睛到一些有点不同的东西。 很难选择最佳答案,但我认为Col. Shrapnel应该得到它,因为一切都被覆盖,甚至进入其他阵列,我不知道自定义库!

但多亏你们所有人:)

感谢有趣的问题。 干得好:

它摆脱了危险的角色,

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

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

你在这里混合一切。
说到数据库,

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

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

逃避用户输入

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

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

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

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

据我所知,使用PDO /预处理语句更安全

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

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

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

准备好的陈述涵盖 - 呃 - 整个2个等级! 很重要;-)

对于其他2个问题,请参阅我之前的回答, 在PHP中,当我向数据库提交字符串时,我应该使用htmlspecialchars()处理非法字符还是使用正则表达式?

现在,函数名称不同,所以我的mysql_query,mysql_fetch_array,mysql_num_rows等不再有效。

这是PHP 用户的另一个严重妄想,一场自然灾害,一场大灾难:

即使使用旧的mysql驱动程序, 也不应该在代码中使用裸API函数 一个人必须把它们放在一些库功能中以供日常使用! (不仅仅是为了使代码更短,更少重复,防错,更一致和可读)。

PDO也是如此!

现在再次提出你的问题。

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

是。

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

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

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

你可以,但你不必。

现在,这一切都安全吗?

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

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

希望我对此事有所了解。

顺便说一句,对于长插入,你可以使用我有一天写的函数,使用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)

我从不打扰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注入神话和谬误 ,或我的书SQL反模式:避免数据库编程的陷阱感兴趣。

是的,:某些东西是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等)。 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.';
}

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

暂无
暂无

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

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