PHP PDO语句可以接受表名或列名作为参数吗?


243

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

Answers:


212

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

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

function buildQuery( $get_var ) 
{
    switch($get_var)
    {
        case 1:
            $tbl = 'users';
            break;
    }

    $sql = "SELECT * FROM $tbl";
}

通过不保留默认大小写或使用返回错误消息的默认大小写,可以确保仅使用要使用的值。


17
+1将选项列入白名单,而不是使用任何动态方法。另一种选择是将可接受的表名映射到具有与潜在用户输入(例如,array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')等)相对应的键的数组
Kzqai 2011年

4
仔细阅读此内容后,我发现这里的示例为无效输入生成了无效的SQL,因为它没有default。如果使用此模式,则应将之一标记casedefault,或添加明确的错误情况,例如default: throw new InvalidArgumentException;
IMSoP 2015年

3
我当时想的很简单if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }。谢谢你的主意。
Phil Tune

2
我很想念mysql_real_escape_string()。也许在这里我可以说出来而无需有人说:“但是PDO并不需要它”
Rolf

另一个问题是动态表名称破坏了SQL检查。
Acyra

143

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

SELECT name FROM my_table WHERE id = :value无论您选择什么计划,计划都将是相同的:value,但是看似相似的SELECT name FROM :table WHERE id = :value计划却无法计划,因为DBMS不知道您实际上要从哪个表中进行选择。

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


1
是的,但没有考虑PDO的prepare语句仿真(可以想像地设置SQL对象标识符的参数,尽管我仍然同意不应该这样做)。
eggyal 2013年

1
@eggyal我想模拟的目的是使标准功能适用于所有DBMS风格,而不是添加全新的功能。标识符的占位符还需要任何DBMS不直接支持的独特语法。PDO是一个相当低的水平的包装,并没有例如报价和SQL生成TOP/ LIMIT/ OFFSET条款,所以这将是一个有点不合适作为特征。
IMSoP 2014年

13

我看到这是一篇过时的文章,但是我发现它很有用,并认为我会分享类似于@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);
}

2
对于短期解决方案是有好处的,但为什么不只是 $pdo->query($sql)
jscripter 2014年

在准备必须绑定变量的查询时,通常会变得不习惯。也可以在这里执行重复读取的重复调用更快,可在此处执行stackoverflow.com/questions/4700623/pdos-query-vs-execute

您的示例中没有重复的电话
您的常识'18

4

使用前者并不能比后者本质上更安全,您需要清理输入,无论它是参数数组的一部分还是简单变量的一部分。因此,在使用后者的形式时$table,如果您确保使用它的内容$table是安全的(字母数字和下划线?),我认为不会有任何问题。


考虑到第一个选项不起作用,您必须使用某种形式的动态查询构建。
诺亚·古德里奇

是的,提到的问题将无法解决。我试图描述为什么尝试这样做并不是那么重要。
亚当·贝莱尔

3

(最新答案,请参阅我的旁注)。

尝试创建“数据库”时,将应用相同的规则。

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

即:

CREATE DATABASE IF NOT EXISTS :database

不管用。请改用安全清单。

旁注:我添加了此答案(作为社区Wiki),因为它通常用来结束问题,有些人在试图绑定数据库而不是表和/或列时发布了与此类似的问题。


0

我的一部分想知道是否可以提供像这样简单的自定义消毒功能:

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

我还没有真正考虑过它,但是似乎删除除字符和下划线之外的所有内容都可能有效。


1
MySQL表名称可以包含其他字符。参见dev.mysql.com/doc/refman/5.0/en/identifiers.html
菲尔

@PhilLaNasa实际上有些人应该捍卫他们(需要参考)。由于大多数DBMS不区分大小写,所以使用不区分大小写的字符来存储名称,例如:MyLongTableName易于阅读,但如果您检查存储的名称,它(可能)将MYLONGTABLENAME不是很可读,因此MY_LONG_TABLE_NAME实际上更具可读性。
mloureiro

有一个很好的理由不具有此功能:您应该很少基于任意输入选择表名。您几乎可以肯定不希望恶意用户将“用户”或“预订”替换为Select * From $table。在这里,白名单或严格的模式匹配(例如“名称以report_开头,后跟1到3位数字”)确实很重要。
IMSoP

0

至于该线程中的主要问题,其他文章都清楚了为什么在准备语句时为什么不能将值绑定到列名,所以这是一个解决方案:

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.