为什么不能将表名传递给准备好的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"

当前回答

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

我有一个函数,它接收两个参数,比如…

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

在里面,我检查我所设置的数组,以确保只有具有“blessed”表的表和列是可访问的:

$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);
}

其他回答

我想知道你是否可以提供你自己的自定义消毒功能,就像这样简单:

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

我还没有真正想过,但似乎除了字符和下划线之外,删除任何东西都可以。

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

我有一个函数,它接收两个参数,比如…

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

在里面,我检查我所设置的数组,以确保只有具有“blessed”表的表和列是可访问的:

$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);
}

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

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中提取的。

要理解为什么绑定表(或列)名不起作用,必须理解预处理语句中的占位符是如何工作的:它们不是简单地作为(适当转义的)字符串替换,并执行结果SQL。相反,一个被要求“准备”语句的DBMS会提出一个完整的查询计划,包括它将如何执行该查询,包括它将使用哪些表和索引,无论您如何填充占位符,它都是一样的。

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

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

简短的回答是否定的,你不能在PDO的Prepared execute语句中使用动态表名、字段名等,因为它会向它们添加引号,这会中断查询。但是如果你可以净化它们,那么你就可以安全地把它们放在查询本身中,就像你使用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;
}