要理解 * 为什么 * 绑定一个表(或列)名不起作用,你必须理解预处理语句中的占位符是如何工作的:它们不是简单地作为(适当地转义的)字符串被替换,并且结果SQL被执行。相反,被要求“准备”语句的DBMS会给出一个完整的查询计划,说明它将如何执行该查询,包括它将使用哪些表和索引,无论如何填充占位符,这些都是相同的。 SELECT name FROM my_table WHERE id = :value的计划将与您替换:value的计划相同,但无法计划看似相似的SELECT name FROM :table WHERE id = :value,因为DBMS不知道您实际上要从哪个表中选择。 这也不是像PDO这样的抽象库可以或应该解决的问题,因为它会破坏预准备语句的两个关键目的:1)允许数据库预先决定查询将如何运行,并多次使用相同的计划;以及2)通过将查询的逻辑与变量输入分离来防止安全问题。
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));
$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;
}
9条答案
按热度按时间mklgxw1f1#
PDO中表名和列名不能被参数替换。
在这种情况下,您只需要手动过滤和清理数据。一种方法是将速记参数传递给将动态执行查询的函数,然后使用
switch()
语句创建用于表名或列名的有效值的白色名单。这样就不会有用户输入直接进入查询。例如:通过不保留默认大小写或使用返回错误消息的默认大小写,可以确保只使用您希望使用的值。
rkkpypqq2#
要理解 * 为什么 * 绑定一个表(或列)名不起作用,你必须理解预处理语句中的占位符是如何工作的:它们不是简单地作为(适当地转义的)字符串被替换,并且结果SQL被执行。相反,被要求“准备”语句的DBMS会给出一个完整的查询计划,说明它将如何执行该查询,包括它将使用哪些表和索引,无论如何填充占位符,这些都是相同的。
SELECT name FROM my_table WHERE id = :value
的计划将与您替换:value
的计划相同,但无法计划看似相似的SELECT name FROM :table WHERE id = :value
,因为DBMS不知道您实际上要从哪个表中选择。这也不是像PDO这样的抽象库可以或应该解决的问题,因为它会破坏预准备语句的两个关键目的:1)允许数据库预先决定查询将如何运行,并多次使用相同的计划;以及2)通过将查询的逻辑与变量输入分离来防止安全问题。
bzzcjhmw3#
我看到这是一个旧帖子,但我发现它很有用,并认为我应该分享一个类似于@kzqai建议的解决方案:
我有一个函数,它接收两个参数,如...
在里面,我检查我设置的数组,以确保只有表和列具有“祝福”表是可访问的:
然后在运行PDO之前的PHP检查看起来像...
4zcjmb1e4#
使用前者并不比使用后者更安全,无论输入是参数数组还是简单变量的一部分,都需要清理输入。所以我不认为对
$table
使用后一种形式有什么错,只要确保$table
的内容是安全的(字母加下划线?)使用之前。ct2axkht5#
当尝试创建“数据库”时,同样的规则也适用。
不能使用预准备语句绑定数据库。
即:
不起作用请改用安全列表。
旁注:我添加了这个答案(作为社区wiki),因为它经常用于关闭问题,有些人在试图绑定数据库而不是表和/或列时发布了类似的问题。
pxyaymoc6#
我的一部分想知道你是否可以提供自己的自定义消毒功能,就像这样简单:
我还没有真正考虑过这个问题,但似乎删除除了字符和下划线之外的任何东西都可能有效。
pkmbmrz77#
至于这个帖子中的主要问题,其他帖子清楚地说明了为什么我们在准备语句时不能将值绑定到列名,所以这里有一个解决方案:
上面只是一个例子,所以不用说,复制->粘贴不会起作用。根据您的需求进行调整。现在,这可能不能提供100%的安全性,但当列名作为动态字符串“进来”时,它允许对列名进行一些控制,并且可以在用户端进行更改。此外,不需要用表的列名和类型构建数组,因为它们是从information_schema中提取的。
vohkndzv8#
针对DDL和DML手动保护代码。
比如:
vybvopom9#
简短的回答是否定的,你不能在PDO的Prepared execute语句中使用动态表名、字段名等,因为它会给它们加上引号,这会中断查询。但是如果你可以清理它们,那么你就可以安全地在查询本身中放置它们,就像你在MySQLi中所做的那样。
正确的方法是使用mysqli的mysqli_真实的_escape_string()函数,因为mysql_real_escape_string被匆忙地从PHP中删除,而没有考虑如何影响动态结构应用程序。