【问题标题】:Can PHP PDO Statements accept the table or column name as parameter?PHP PDO 语句可以接受表名或列名作为参数吗?
【发布时间】:2017-11-25 09:07:45
【问题描述】:

为什么我不能将表名传递给准备好的 PDO 语句?

$stmt = $dbh->prepare('SELECT * FROM :table WHERE 1');
if ($stmt->execute(array(':table' => 'users'))) {
    var_dump($stmt->fetchAll());
}

还有其他安全的方法可以将表名插入 SQL 查询吗?有了安全,我的意思是我不想做

$sql = "SELECT * FROM $table WHERE 1"

【问题讨论】:

    标签: php pdo


    【解决方案1】:

    表名和列名不能被 PDO 中的参数替换。

    在这种情况下,您只需手动过滤和清理数据。一种方法是将速记参数传递给将动态执行查询的函数,然后使用switch() 语句创建用于表名或列名的有效值的白名单。这样,任何用户输入都不会直接进入查询。比如:

    function buildQuery( $get_var ) 
    {
        switch($get_var)
        {
            case 1:
                $tbl = 'users';
                break;
        }
    
        $sql = "SELECT * FROM $tbl";
    }
    

    通过不保留默认情况或使用返回错误消息的默认情况,您可以确保只使用您想要使用的值。

    【讨论】:

    • +1 用于白名单选项,而不是使用任何类型的动态方法。另一种选择可能是将可接受的表名映射到具有与潜在用户输入相对应的键的数组(例如array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data') 等)
    • 读到这里,我突然想到这里的示例为错误的输入生成了无效的 SQL,因为它没有default。如果使用此模式,您应该将其中一个cases 标记为default,或者添加一个明确的错误案例,例如default: throw new InvalidArgumentException;
    • 我在想一个简单的if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }。感谢您的想法。
    • 我想念mysql_real_escape_string()。也许在这里我可以说出来,而不会有人插嘴说“但是 PDO 不需要它”
    • 另一个问题是动态表名破坏了 SQL 检查。
    【解决方案2】:

    要了解为什么绑定表(或列)名称不起作用,您必须了解准备好的语句中的占位符是如何工作的:它们不是简单地替换为(适当转义的)字符串,并执行生成的 SQL。相反,要求“准​​备”语句的 DBMS 会针对如何执行该查询提出完整的查询计划,包括它将使用哪些表和索引,无论您如何填写占位符,这都是相同的。

    SELECT name FROM my_table WHERE id = :value 的计划将与您替换 :value 的计划相同,但无法计划看似相似的 SELECT name FROM :table WHERE id = :value,因为 DBMS 不知道您实际上要从哪个表中选择。

    这也不是像 PDO 这样的抽象库可以或应该解决的问题,因为它会破坏准备好的语句的两个关键目的:1) 允许数据库提前决定如何运行查询,以及多次使用同一个计划; 2) 通过将查询逻辑与变量输入分开来防止安全问题。

    【讨论】:

    • 是的,但不考虑 PDO 的 prepare 语句模拟(这可以可能会参数化 SQL 对象标识符,尽管我仍然同意它可能不应该这样做)。
    • @eggyal 我猜模拟的目的是使标准功能适用于所有 DBMS 风格,而不是添加全新的功能。标识符的占位符还需要任何 DBMS 不直接支持的独特语法。 PDO 是一个相当低级的包装器,例如不为 TOP/LIMIT/OFFSET 子句提供和 SQL 生成,所以这作为一个特性有点不合适。
    【解决方案3】:

    我看到这是一篇旧帖子,但我发现它很有用,并认为我会分享一个类似于 @kzqai 建议的解决方案:

    我有一个接收两个参数的函数,比如...

    function getTableInfo($inTableName, $inColumnName) {
        ....
    }
    

    在里面我检查了我设置的数组,以确保只有带有“祝福”表的表和列可以访问:

    $allowed_tables_array = array('tblTheTable');
    $allowed_columns_array['tblTheTable'] = array('the_col_to_check');
    

    那么在运行 PDO 之前的 PHP 检查看起来像......

    if(in_array($inTableName, $allowed_tables_array) && in_array($inColumnName,$allowed_columns_array[$inTableName]))
    {
        $sql = "SELECT $inColumnName AS columnInfo
                FROM $inTableName";
        $stmt = $pdo->prepare($sql); 
        $stmt->execute();
        $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    

    【讨论】:

    • 适合简短的解决方案,但为什么不只是$pdo->query($sql)
    • 在准备必须绑定变量的查询时大多是出于习惯。还可以通过在此处执行 stackoverflow.com/questions/4700623/pdos-query-vs-execute 来更快地读取重复调用
    • 您的示例中没有重复调用
    【解决方案4】:

    使用前者本质上并不比后者更安全,您需要清理输入,无论它是参数数组的一部分还是简单变量。所以我认为将后一种形式与$table 一起使用没有任何问题,前提是您在使用之前确保$table 的内容是安全的(字母数字加下划线?)。

    【讨论】:

    • 考虑到第一个选项不起作用,您必须使用某种形式的动态查询构建。
    • 是的,上面提到的问题不起作用。我试图描述为什么即使尝试这样做也不是非常重要。
    【解决方案5】:

    (迟到的答案,请查阅我的附注)。

    在尝试创建“数据库”时适用相同的规则。

    您不能使用准备好的语句来绑定数据库。

    即:

    CREATE DATABASE IF NOT EXISTS :database
    

    不会工作。请改用安全列表。

    旁注:我添加了这个答案(作为社区 wiki),因为它经常用来关闭问题,有些人在尝试绑定 数据库 而不是表格和/或列。

    【讨论】:

      【解决方案6】:

      我的一部分想知道您是否可以像这样简单地提供自己的自定义清理功能:

      $value = preg_replace('/[^a-zA-Z_]*/', '', $value);
      

      我还没有真正考虑过,但似乎删除除字符和下划线之外的任何内容都可以。

      【讨论】:

      • MySQL 表名可以包含其他字符。见dev.mysql.com/doc/refman/5.0/en/identifiers.html
      • @PhilLaNasa 实际上一些 捍卫他们应该(需要参考)。由于大多数 DBMS 不区分大小写,以非区分字符存储名称,例如:MyLongTableName 它很容易正确阅读,但如果您检查存储的名称,它(可能)会是 MYLONGTABLENAME,这不是很可读,所以MY_LONG_TABLE_NAME 实际上更具可读性。
      • 有一个很好的理由不把它作为一个函数:你应该很少根据任意输入来选择表名。您几乎肯定不希望恶意用户将“用户”或“预订”替换为Select * From $table。白名单或严格的模式匹配(例如“名称以 report_ 开头,后跟 1 到 3 位数字”)在这里确实很重要。
      【解决方案7】:

      至于这个帖子的主要问题,其他帖子清楚地说明了为什么我们在准备语句时不能将值绑定到列名,所以这里有一个解决方案:

      class myPdo{
          private $user   = 'dbuser';
          private $pass   = 'dbpass';
          private $host   = 'dbhost';
          private $db = 'dbname';
          private $pdo;
          private $dbInfo;
          public function __construct($type){
              $this->pdo = new PDO('mysql:host='.$this->host.';dbname='.$this->db.';charset=utf8',$this->user,$this->pass);
              if(isset($type)){
                  //when class is called upon, it stores column names and column types from the table of you choice in $this->dbInfo;
                  $stmt = "select distinct column_name,column_type from information_schema.columns where table_name='sometable';";
                  $stmt = $this->pdo->prepare($stmt);//not really necessary since this stmt doesn't contain any dynamic values;
                  $stmt->execute();
                  $this->dbInfo = $stmt->fetchAll(PDO::FETCH_ASSOC);
              }
          }
          public function pdo_param($col){
              $param_type = PDO::PARAM_STR;
              foreach($this->dbInfo as $k => $arr){
                  if($arr['column_name'] == $col){
                      if(strstr($arr['column_type'],'int')){
                          $param_type = PDO::PARAM_INT;
                          break;
                      }
                  }
              }//for testing purposes i only used INT and VARCHAR column types. Adjust to your needs...
              return $param_type;
          }
          public function columnIsAllowed($col){
              $colisAllowed = false;
              foreach($this->dbInfo as $k => $arr){
                  if($arr['column_name'] === $col){
                      $colisAllowed = true;
                      break;
                  }
              }
              return $colisAllowed;
          }
          public function q($data){
              //$data is received by post as a JSON object and looks like this
              //{"data":{"column_a":"value","column_b":"value","column_c":"value"},"get":"column_x"}
              $data = json_decode($data,TRUE);
              $continue = true;
              foreach($data['data'] as $column_name => $value){
                  if(!$this->columnIsAllowed($column_name)){
                       $continue = false;
                       //means that someone possibly messed with the post and tried to get data from a column that does not exist in the current table, or the column name is a sql injection string and so on...
                       break;
                   }
              }
              //since $data['get'] is also a column, check if its allowed as well
              if(isset($data['get']) && !$this->columnIsAllowed($data['get'])){
                   $continue = false;
              }
              if(!$continue){
                  exit('possible injection attempt');
              }
              //continue with the rest of the func, as you normally would
              $stmt = "SELECT DISTINCT ".$data['get']." from sometable WHERE ";
              foreach($data['data'] as $k => $v){
                  $stmt .= $k.' LIKE :'.$k.'_val AND ';
              }
              $stmt = substr($stmt,0,-5)." order by ".$data['get'];
              //$stmt should look like this
              //SELECT DISTINCT column_x from sometable WHERE column_a LIKE :column_a_val AND column_b LIKE :column_b_val AND column_c LIKE :column_c_val order by column_x
              $stmt = $this->pdo->prepare($stmt);
              //obviously now i have to bindValue()
              foreach($data['data'] as $k => $v){
                  $stmt->bindValue(':'.$k.'_val','%'.$v.'%',$this->pdo_param($k));
                  //setting PDO::PARAM... type based on column_type from $this->dbInfo
              }
              $stmt->execute();
              return $stmt->fetchAll(PDO::FETCH_ASSOC);//or whatever
          }
      }
      $pdo = new myPdo('anything');//anything so that isset() evaluates to TRUE.
      var_dump($pdo->q($some_json_object_as_described_above));
      

      以上只是一个例子,不用说,复制->粘贴是行不通的。根据您的需要进行调整。 现在这可能无法提供 100% 的安全性,但它允许在列名作为动态字符串“进入”时对它们进行一些控制,并且可以在用户端进行更改。此外,由于它们是从 information_schema 中提取的,因此无需使用您的表列名称和类型构建一些数组。

      【讨论】:

        【解决方案8】:

        简短的回答是“否”,您不能在 PDO 的 Prepared 执行语句中使用动态表名、字段名等,因为它会向它们添加引号,这会破坏查询。但是,如果您可以清理它们,那么您可以安全地将它们直接放入查询本身,就像使用 MySQLi 一样。

        执行此操作的正确方法是使用 mysqli 的 mysqli_real_escape_string() 函数,因为 mysql_real_escape_string 被匆忙从 PHP 中删除,没有考虑它如何影响动态结构应用程序。

        $unsanitized_table_name = "users' OR '1'='1"; //SQL Injection attempt
        $sanitized_table_name = sanitize_input($unsanitized_table_name);
        
        $stmt = $dbh->prepare("SELECT * FROM {$unsanitized_table_name} WHERE 1"); //<--- REALLY bad idea
        $stmt = $dbh->prepare("SELECT * FROM {$sanitized_table_name} WHERE 1"); //<--- Not ideal but hey, at least you're safe.
        
        //PDO Cant sanitize everything so we limp along with mysqli instead
        function sanitize_input($string)
        {
           $mysqli = new mysqli("localhost","UsahName","Passerrrd");
           $string = $mysqli->real_escape_string($string);
        
           return $string;
        }
        

        【讨论】:

          猜你喜欢
          相关资源
          最近更新 更多