【问题标题】:Switching mySQL insert and update statements to PDO prepared statements to prevent SQL injection将 mySQL 插入和更新语句切换为 PDO 准备语句以防止 SQL 注入
【发布时间】:2012-11-27 11:02:04
【问题描述】:

我正在尝试将这些 mySQl INSERT INTO 和 Update 语句切换为 PDO 准备语句(主要是为了防止 SQL 注入),但我在正确使用语法时遇到了一些困难。

我目前使用 2 种类型的 INSERT/Update 语句:

声明 1 - 名称是硬编码的

$qry = "INSERT INTO customer_info(fname, lname, email, user_name, password)
VALUES('$_POST[fname]','$_POST[lname]','$_POST[email]','$user_name','".sha1($salt + $_POST['password'])."')"; 
$result = @mysql_query($qry)

语句 2 - 动态添加名称

大多数名称都是动态添加的,而不是列出每个元素的名称(名称被引用为 $fieldlist 或 $setlist,值是 $vallist)。唯一硬编码的名称/值是 user_id 或数组。我在下面包含了完整的代码。

$result = mysql_query('UPDATE fit_table  SET '.$setlist.' WHERE user_id='.$user_id);
if (mysql_affected_rows()==0) {
$result = mysql_query('INSERT INTO fit_table ('.$fieldlist.') VALUES ('.$vallist.')'); };   

这是我尝试过的:

声明 1 - 基于此帖子 https://stackoverflow.com/a/60530/1056713

$stmt = $conn->prepare("INSERT INTO customer_info VALUES(:fname, :lname, :email, :user_name, :password)");
$stmt->bindValue(':fname', $fname);
$stmt->bindValue(':lname', $lname);
$stmt->bindValue(':email', $email);
$stmt->bindValue(':user_name', $user_name);
$stmt->bindValue(':password ', $password);
$stmt->execute();

声明 2 - 基于此 PDO 包装器 https://github.com/Xeoncross/DByte/blob/master/DB.php(在此帖子中引用 https://stackoverflow.com/a/12500462/1056713

static function insert($fit_table, array $fieldlist){
$query = "INSERT INTO`$fit_table`(`" . implode('`,`', array_keys('.$fieldlist.')). '`) 
VALUES(' . rtrim(str_repeat('?,', count($fieldlist = array_values('.$vallist.'))), ',') . ')';
return DB::$p
? DB::column($query . 'RETURNING` user_id `', $fieldlist)
: (DB::query($query, $fieldlist) ? static::$c->lastInsertId() : NULL);
}

语句2的完整代码(这是目前动态添加名称的方式)

// INSERT    
$fieldlist=$vallist='';
foreach ($_POST as $key => $value) {
    if ($key=='pants_waistband'){$value= implode(',',$value);}        
    $fieldlist.=$key.',';
    $vallist.='\''.($value).'\',';
}
$fieldlist=substr($fieldlist, 0, -1);
$vallist=substr($vallist, 0, -1);
$fieldlist.=', user_id';
$vallist.=','.$user_id;
// UPDATE
$setlist='';
foreach ($_POST as $key => $value) {
    if ($key=='pants_waistband'){$value= implode(',',$value);}  
    $setlist.=$key .'=\''.$value.'\',';
}
$setlist=substr($setlist, 0, -1); 

$result = mysql_query('UPDATE fit_table SET '.$setlist.' WHERE user_id='.$user_id);
if (mysql_affected_rows()==0) {
$result = mysql_query('INSERT INTO fit_table ('.$fieldlist.') VALUES ('.$vallist.')');}  

【问题讨论】:

  • 很高兴您正在进行过渡,但您的问题有点不清楚。你能告诉我们更多关于到底出了什么问题吗?
  • 谢谢 :-) 我对 PDO 和 Prepared Statements 非常不熟悉,所以我确信问题在于我尝试将我的代码应用于我在网上找到的示例的方式。任何指导都会有所帮助,因为我发现 php.net 的手册有点过于技术化。
  • 除了白名单之外,没有其他方法可以保护字段和表名。 没有例外。所以你必须保留允许的字段列表并且只将它们添加到查询中,而不是盲目地添加来自$_POST的所有内容
  • @zerkms 谢谢!我逐渐意识到:-(

标签: php mysql pdo prepared-statement sql-injection


【解决方案1】:

看,白名单并不像看起来那么无聊!
动态查询很棒,没有理由放弃这个想法。
至少您可以将其设为半动态,以避免所有这些重复。

PDO 有一件很棒的事情——它可以接受一个带有值的数组,从而不需要重复绑定。 它可以像

一样简单
$stmt = $conn->prepare('INSERT INTO customer_info VALUES(?,?,?,?,?)');
$stmt->execute($_POST);

如果 $_POST 包含按正确顺序的确切数量的字段,它将被执行。 但是,一旦我们需要查询中的字段名称,它就会失去所有自动化(如您自己的答案)或变得不安全(如您之前的动态代码)。

好吧,让我们让它既安全又动态。
您唯一需要的是一个包含允许字段名称的数组,这将是我们的白名单。
然后,您可以使用此数组循环 $_POST,动态创建查询。
这是一个自动化流程的功能:
它需要三个参数,但实际上只使用一个

function pdoSet($fields, &$values, $source = array()) {
  $set = '';
  $values = array();
  if (!$source) $source = &$_POST;
  foreach ($fields as $field) {
    if (isset($source[$field])) {
      $set.="`$field`=:$field, ";
      $values[$field] = $source[$field];
    }
  }
  return substr($set, 0, -2); 
}

它将返回看起来像这样的字符串

`field1`=?,`field2`=?,`field3`=?

并将填充 $values 数组以供 PDO 查询使用。

请注意,Mysql 允许对 INSERT 和 UPDATE 查询使用 SET 语法 - 不需要 VALUES 语法。因此,两种类型的一个功能。

对于插入,它就像

一样简单
$fields = array("fname", "lname", "email", "user_name");
$stmt = $dbh->prepare("UPDATE users SET ".pdoSet($fields,$values));
$stmt->execute($values);

对于任何数量的字段,它将保持相同的 3 行!

对于更新,它需要更长的代码。我们需要向查询添加一些条件,以及向 $values 数组添加另一个成员。

$fields = array("fname", "lname", "email", "user_name");
$stmt = $dbh->prepare("UPDATE users SET ".pdoSet($fields,$values)." WHERE id = :id");
$values["id"] = $_POST['id'];
$stmt->execute($values);

剩下的唯一问题是如何添加尚未包含在 $_POST 数组中的自定义字段。
我只是在准备之前直接将它们添加到那里:

$_POST['password'] = sha1($_POST['email'].$_POST['password']);

希望这是您所要求的。

只需澄清一件事。
准备好的语句不足以阻止注入,您的案例就是一个很好的例子。它们只处理数据,但保护字段名称是您的负担。 然而,您使用的旧 mysql 方式并没有错。您的代码只是缺少相同的白名单(当然还有正确的数据格式)。但是如果添加,它会让你的 mysql 查询和 PDO 一样安全。

【讨论】:

  • 当您说“Mysql 允许对 INSERT 和 UPDATE 查询使用 SET 语法”时,这是否意味着我不必将关键字“insert”放入 mysql 创建表条目中?
  • 所以不会让我只编辑一个字符,所以如果有任何 PHP 新手阅读此内容,'VA?LUES' 显然意味着是 'VALUES' ;)
  • 谢谢,已修复!
  • 顺便说一句,在 SO 上写出最具吸引力和易于阅读的答案之一的荣誉。它让我更容易理解作为菜鸟的东西(或者当我重温我非常生锈的 PHP 时):)
【解决方案2】:

我发现白名单名称比其他方法安全得多(感谢包括@zerkms 在内的几个人的帮助),并希望分享完成的声明。

它现在可以正常工作,并且包括使用 PDO 连接到数据库所需的方法。我还切换了语句中使用的 db 用户帐户,因为我了解到最好使用具有有限权限(只能 SELECT、INSERT 和 UPDATE)的帐户,以尽量减少黑客可能造成的损害。

try { 
      $conn = new PDO('mysql:host=localhost;dbname=dbname', 'Username', 'MyPassword');
      $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

      $stmt = $conn->prepare('INSERT INTO customer_info (fname...) VALUES(:fname...)');
      $stmt->bindParam(':fname', $_POST['fname'], PDO::PARAM_STR);
      $stmt->execute();   
    } catch(PDOException $e) {
  echo $e->getMessage();
}

【讨论】:

    猜你喜欢
    • 2012-08-07
    • 2012-03-19
    • 2012-06-28
    • 2017-09-19
    • 1970-01-01
    • 1970-01-01
    • 2012-01-05
    相关资源
    最近更新 更多