为什么我不能将表名传递给准备好的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"
答案 0 :(得分:197)
表格和列名称不能被PDO中的参数替换。
在这种情况下,您只需手动过滤和清理数据。一种方法是将速记参数传递给将动态执行查询的函数,然后使用switch()
语句创建用于表名或列名的有效值的白名单。这样,用户输入就不会直接进入查询。例如:
function buildQuery( $get_var )
{
switch($get_var)
{
case 1:
$tbl = 'users';
break;
}
$sql = "SELECT * FROM $tbl";
}
通过不保留默认大小写或使用返回错误消息的默认大小写,您可以确保只使用您想要使用的值。
答案 1 :(得分:132)
要理解为什么绑定表(或列)名称不起作用,您必须了解预处理语句中的占位符如何工作:它们不能简单地替换为(适当转义的)字符串,以及生成的SQL执行。相反,DBMS要求“准备”一个语句,它会提供一个完整的查询计划,说明它将如何执行该查询,包括它将使用哪些表和索引,无论你如何填写占位符,这都是相同的。 / p>
SELECT name FROM my_table WHERE id = :value
的计划与:value
的替换相同,但看似相似的SELECT name FROM :table WHERE id = :value
无法计划,因为DBMS不知道你实际上是哪一张表要选择。
这不是像PDO这样的抽象库可以或应该解决的问题,因为它会破坏准备语句的两个关键目的:1)允许数据库事先决定如何运行查询,以及多次使用同一计划; 2)通过将查询逻辑与变量输入分开来防止安全问题。
答案 2 :(得分:12)
我看到这是一篇很老的帖子,但我发现它很有用,并且我认为我会分享一个类似于@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);
}
答案 3 :(得分:4)
使用前者本身并不比后者更安全,您需要清理输入,无论它是参数数组还是简单变量的一部分。因此,如果您在使用$table
之前确保$table
的内容是安全的(alphanum加下划线?),我认为使用后一种形式{{1}}没有任何问题。
答案 4 :(得分:2)
(最新答案,请参阅我的旁注)。
在尝试创建“数据库”时,同样的规则也适用。
您不能使用准备好的语句来绑定数据库。
即:
CREATE DATABASE IF NOT EXISTS :database
不起作用。请改用安全列表。
旁注::我添加了此答案(作为社区Wiki),因为它通常用来关闭问题,有些人在试图绑定数据库< / strong>,而不是表和/或列。
答案 5 :(得分:0)
部分我想知道您是否可以提供您自己的自定义消毒功能,如下所示:
$value = preg_replace('/[^a-zA-Z_]*/', '', $value);
我还没有真正考虑过它,但似乎删除除了字符和下划线之外的任何内容都可能有效。
答案 6 :(得分: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-gt; paste不会起作用。根据您的需求进行调整 现在这可能无法提供100%的安全性,但是当它们进入&#34;时,它允许对列名称进行一些控制。作为动态字符串,可以在用户端更改。此外,不需要使用表列名称和类型构建一些数组,因为它们是从information_schema中提取的。