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

Can PHP PDO Statements accept the table or column name as parameter?

提问人:Jrgns 提问时间:10/8/2008 最后编辑:RahulJrgns 更新时间:5/16/2023 访问量:99501

问:

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

评论


答:

5赞 Adam Bellaire 10/8/2008 #1

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

评论

0赞 Noah Goodrich 10/8/2008
考虑到第一个选项不起作用,您必须使用某种形式的动态查询生成。
0赞 Adam Bellaire 10/8/2008
是的,提到的问题行不通。我试图描述为什么尝试这样做并不是很重要。
241赞 Noah Goodrich 10/8/2008 #2

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

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

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

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

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

评论

25赞 Kzqai 12/23/2011
+1 用于白名单选项,而不是使用任何类型的动态方法。另一种选择可能是将可接受的表名映射到具有与潜在用户输入相对应的键的数组(例如 等)array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')
4赞 IMSoP 10/22/2015
阅读本文后,我突然想到这里的示例为错误的输入生成了无效的 SQL,因为它没有 .如果使用此模式,则应将其中一个 标记为 ,或添加显式错误情况,例如defaultcasedefaultdefault: throw new InvalidArgumentException;
3赞 Phil Tune 3/3/2016
我在想一个简单的.谢谢你的想法。if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }
3赞 Rolf 10/20/2018
我想念。也许在这里我可以说出来,而不会有人跳进来说“但你不需要 PDO 它”mysql_real_escape_string()
1赞 Acyra 7/20/2019
另一个问题是动态表名会破坏 SQL 检查。
167赞 IMSoP 4/14/2013 #3

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

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

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

评论

1赞 eggyal 12/28/2013
没错,但没有考虑 PDO 的 prepare 语句仿真(可以想象,它可以参数化 SQL 对象标识符,尽管我仍然同意它可能不应该)。
1赞 IMSoP 1/2/2014
@eggyal 我猜仿真的目的是使标准功能适用于所有 DBMS 风格,而不是添加全新的功能。标识符的占位符还需要任何 DBMS 都不直接支持的不同语法。PDO 是一个非常低级的包装器,例如没有为 // 子句提供和 SQL 生成,所以这作为一个功能有点不合适。TOPLIMITOFFSET
14赞 Don 5/1/2013 #4

我看到这是一篇旧帖子,但我发现它很有用,并认为我会分享一个类似于@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赞 jscripter 12/15/2014
适合简短的解决方案,但为什么不只是$pdo->query($sql)
0赞 Don 4/13/2015
主要是出于准备必须绑定变量的查询的习惯。另请阅读 重复调用更快 w/ 执行此处 stackoverflow.com/questions/4700623/pdos-query-vs-execute
1赞 Your Common Sense 3/21/2018
示例中没有重复调用
-1赞 Phil LaNasa 4/29/2014 #5

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

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

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

评论

1赞 Phil 4/29/2014
MySQL表名可以包含其他字符。查看 dev.mysql.com/doc/refman/5.0/en/identifiers.html
0赞 mloureiro 8/11/2015
@PhilLaNasa实际上有些人捍卫他们应该(需要参考)。由于大多数 DBMS 不区分大小写,以非区分字符存储名称,例如:它很容易正确阅读,但如果您检查存储的名称,它(可能)不是很可读,所以实际上更可读。MyLongTableNameMYLONGTABLENAMEMY_LONG_TABLE_NAME
0赞 IMSoP 3/21/2017
有一个很好的理由不将其作为函数:您很少应该根据任意输入选择表名。几乎可以肯定的是,您不希望恶意用户将“users”或“bookings”替换为 。白名单或严格的模式匹配(例如,“名称以 report_ 开头,后跟 1 到 3 位数字”)在这里确实是必不可少的。Select * From $table
-1赞 man 9/9/2014 #6

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

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中提取的,因此无需使用表列名称和类型构建一些数组。

5赞 Funk Forty Niner #7

(迟到的回答,请查阅我的旁注)。

尝试创建“数据库”时,同样的规则也适用。

不能使用预准备语句绑定数据库。

即:

CREATE DATABASE IF NOT EXISTS :database

行不通。请改用安全列表。

旁注:我添加了这个答案(作为一个社区维基),因为它经常被用来结束问题,有些人在尝试绑定数据库而不是表和/或列时发布了类似的问题。

-3赞 totalnoob 11/5/2021 #8

简短的回答是否定的,您不能在带有 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;
}

评论

0赞 Dharman 3/29/2022
请记住,不能用于在 SQL 中格式化 SQL 标识符。顾名思义,它仅用于格式化字符串文字。您滥用了此功能。即使您不同意其他用户名的回复,也不要调用他们的用户名。real_escape_string
-2赞 Hakan 5/16/2023 #9

手动保护您的代码免受 DDL 和 DML 的攻击。

喜欢:

function protect($a){//sql protect
    $a=trim(preg_replace("#insert |delete |update | --|drop |replace |alter |modify |create |select #sui"," ", $a));
    return $a;
}

评论

2赞 Your Common Sense 5/16/2023
这是一种非常糟糕和不充分的方法。请不要在安全方面使用黑名单方法。只应使用白名单。