Pourquoi ne puis-je pas transmettre le nom de la table à une instruction PDO préparée?
$stmt = $dbh->prepare('SELECT * FROM :table WHERE 1');
if ($stmt->execute(array(':table' => 'users'))) {
var_dump($stmt->fetchAll());
}
Existe-t-il un autre moyen sûr d’insérer un nom de table dans une requête SQL? Avec safe, je veux dire que je ne veux pas faire
$sql = "SELECT * FROM $table WHERE 1"
Les noms de table et de colonne NE PEUVENT PAS être remplacés par des paramètres dans PDO.
Dans ce cas, vous voudrez simplement filtrer et assainir les données manuellement. Une façon de faire est de passer des paramètres abrégés à la fonction qui exécutera la requête dynamiquement, puis utilisera une instruction switch()
pour créer une liste blanche de valeurs valides à utiliser pour le nom de la table ou de la colonne. . De cette façon, aucune entrée d'utilisateur ne va directement dans la requête. Donc par exemple:
function buildQuery( $get_var )
{
switch($get_var)
{
case 1:
$tbl = 'users';
break;
}
$sql = "SELECT * FROM $tbl";
}
En ne laissant aucun cas par défaut ou en utilisant un cas par défaut qui renvoie un message d'erreur, vous vous assurez que seules les valeurs que vous souhaitez utiliser sont utilisées.
Pour comprendre pourquoi relier un nom de table (ou de colonne) ne fonctionne pas, vous devez comprendre comment fonctionnent les espaces réservés dans les instructions préparées: ils ne sont pas simplement substitués par des chaînes (échappées de manière appropriée), et résultat SQL exécuté. Au lieu de cela, un SGBD invité à "préparer" une instruction fournit un plan de requête complet indiquant la manière dont il exécuterait cette requête, y compris les tables et les index qu’il utiliserait, qui seront identiques, quelle que soit la manière dont vous remplissez les espaces réservés.
Le plan pour SELECT name FROM my_table WHERE id = :value
sera identique à ce que vous substituez à :value
, mais l'apparence similaire SELECT name FROM :table WHERE id = :value
ne peut pas être planifié, car le SGBD n'a aucune idée de la table dans laquelle vous allez réellement sélectionner.
Ce n'est pas quelque chose qu'une bibliothèque d'abstraction comme PDO peut ou devrait contourner, car elle irait à l'encontre des deux objectifs clés des instructions préparées: 1) permettre à la base de données de décider à l'avance de la manière dont une requête sera exécutée et d'utiliser le même planifier plusieurs fois; et 2) éviter les problèmes de sécurité en séparant la logique de la requête de l'entrée de variable.
Je vois qu'il s'agit d'un ancien message, mais je l'ai trouvé utile et je pensais partager une solution similaire à celle suggérée par @kzqai:
J'ai une fonction qui reçoit deux paramètres comme ...
function getTableInfo($inTableName, $inColumnName) {
....
}
À l'intérieur, je vérifie les tableaux que j'ai configurés pour m'assurer que seules les tables et les colonnes contenant des tables "bénies" sont accessibles:
$allowed_tables_array = array('tblTheTable');
$allowed_columns_array['tblTheTable'] = array('the_col_to_check');
Ensuite, la vérification PHP avant d'exécuter PDO ressemble à ...
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);
}
L'utilisation de la première n'est pas intrinsèquement plus sûre que la dernière, vous devez donc purifier l'entrée, qu'elle fasse partie d'un tableau de paramètres ou d'une simple variable. Donc, je ne vois rien de mal à utiliser cette dernière forme avec $table
, à condition de s’assurer que le contenu de $table
est en sécurité (caractères alphanum plus?) avant de l’utiliser.
(réponse tardive, consultez ma note latérale).
La même règle s'applique lorsque vous essayez de créer une "base de données".
Vous ne pouvez pas utiliser une instruction préparée pour lier une base de données.
C'est à dire.:
CREATE DATABASE IF NOT EXISTS :database
ne fonctionnera pas. Utilisez plutôt une liste de sécurité.
Note latérale: J'ai ajouté cette réponse (en tant que wiki de communauté) car il était souvent utilisé pour fermer des questions avec, où certaines personnes posaient des questions similaires à celles-ci en essayant de lier un base de données et non une table et/ou une colonne.
Une partie de moi se demande si vous pourriez fournir votre propre fonction de désinfection personnalisée aussi simple que cela:
$value = preg_replace('/[^a-zA-Z_]*/', '', $value);
Je n'y ai pas vraiment réfléchi, mais il me semble que tout supprimer, à l'exception des caractères et des soulignés, peut fonctionner.
En ce qui concerne la question principale de ce fil de discussion, les autres publications expliquent pourquoi nous ne pouvons pas associer des valeurs aux noms de colonnes lors de la préparation des instructions. Voici donc une solution:
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));
Ce qui précède n’est qu’un exemple. Inutile de dire que copier-> coller ne fonctionnera pas. Ajustez à vos besoins. Maintenant, cela ne fournit peut-être pas une sécurité à 100%, mais cela permet de contrôler les noms de colonnes quand ils "entrent" en tant que chaînes dynamiques et peuvent être modifiés du côté des utilisateurs. De plus, il n'est pas nécessaire de construire un tableau avec les noms et les types de colonnes de votre table car ils sont extraits de la classe information_schema.