Les instructions PHP PDO peuvent-elles accepter le nom de la table ou de la colonne comme paramètre?

243

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 coffre-fort, je veux dire que je ne veux pas faire

$sql = "SELECT * FROM $table WHERE 1"
Jrgns
la source

Réponses:

212

Les noms de table et de colonne NE PEUVENT PAS être remplacés par des paramètres dans PDO.

Dans ce cas, vous souhaiterez simplement filtrer et désinfecter les données manuellement. Pour cela, vous pouvez passer des paramètres abrégés à la fonction qui exécutera la requête de manière dynamique, puis utiliser une switch()instruction pour créer une liste blanche de valeurs valides à utiliser pour le nom de la table ou le nom de la colonne. De cette façon, aucune entrée utilisateur ne va jamais directement dans la requête. Ainsi, 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.

Noah Goodrich
la source
17
+1 pour les options de liste blanche au lieu d'utiliser tout type de méthode dynamique. Une autre alternative pourrait être de mapper des noms de table acceptables sur un tableau avec des clés qui correspondent à l'entrée potentielle de l'utilisateur (par exemple, array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')etc.)
Kzqai
4
En lisant cela, il me vient à l'esprit que l'exemple ici génère du SQL non valide pour une mauvaise entrée, car il n'en a pas default. Si vous utilisez ce modèle, vous devez soit étiqueter l'un de vos cases comme default, soit ajouter un cas d'erreur explicite tel quedefault: throw new InvalidArgumentException;
IMSoP
3
Je pensais à un simple if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Merci pour l'idée.
Phil Tune
2
Ça me manque mysql_real_escape_string(). Peut-être qu'ici je peux le dire sans que quelqu'un se jette dedans et dise "Mais vous n'en avez pas besoin avec AOP"
Rolf
L'autre problème est que les noms de tables dynamiques interrompent l'inspection SQL.
Acyra
143

Pour comprendre pourquoi la liaison d'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 en tant que chaînes (correctement échappées) et le SQL résultant est exécuté. Au lieu de cela, un SGBD a demandé de "préparer" une instruction propose un plan de requête complet sur la façon dont il exécuterait cette requête, y compris les tables et les index qu'il utiliserait, qui seront les mêmes quelle que soit la façon dont vous remplissez les espaces réservés.

Le plan pour SELECT name FROM my_table WHERE id = :valuesera le même que ce que vous remplacez :value, mais SELECT name FROM :table WHERE id = :valueil ne peut pas être prévu d' apparence similaire , car le SGBD n'a aucune idée de la table à partir de 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 cela irait à l'encontre des 2 objectifs clés des instructions préparées: 1) pour 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 la même chose planifier plusieurs fois; et 2) pour éviter les problèmes de sécurité en séparant la logique de la requête de l'entrée variable.

IMSoP
la source
1
Certes, mais ne tient pas compte de l'émulation de l'instruction prepare de PDO (qui pourrait éventuellement paramétrer les identificateurs d'objet SQL, bien que je convienne toujours que cela ne devrait probablement pas).
eggyal
1
@eggyal Je suppose que l'émulation vise à faire fonctionner les fonctionnalités standard sur toutes les versions du SGBD, plutôt que d'ajouter des fonctionnalités complètement nouvelles. Un espace réservé pour les identifiants aurait également besoin d'une syntaxe distincte qui n'est directement prise en charge par aucun SGBD. PDO est un wrapper de bas niveau, et n'offre par exemple pas de génération et de génération SQL pour les clauses TOP/ LIMIT/ OFFSET, donc ce serait un peu déplacé en tant que fonctionnalité.
IMSoP
13

Je vois que c'est un ancien message, mais je l'ai trouvé utile et j'ai pensé partager une solution similaire à ce que @kzqai a suggéré:

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 avec 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);
}
Don
la source
2
bon pour une solution courte, mais pourquoi pas$pdo->query($sql)
jscripter
Surtout par habitude lors de la préparation de requêtes qui doivent lier une variable. Lisez également les appels répétés sont plus rapides w / exécutez ici stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don
il n'y a pas d'appels répétés dans votre exemple
Your Common Sense
4

L'utilisation de la première n'est pas intrinsèquement plus sûre que la seconde, vous devez nettoyer 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 ce dernier formulaire avec $table, à condition de vous assurer que le contenu de $tableest sûr (alphanum plus les traits de soulignement?) Avant de l'utiliser.

Adam Bellaire
la source
Étant donné que la première option ne fonctionnera pas, vous devez utiliser une forme de création de requête dynamique.
Noah Goodrich
Oui, la question mentionne que cela ne fonctionnera pas. J'essayais de décrire pourquoi il n'était pas terriblement important d'essayer même de le faire de cette façon.
Adam Bellaire,
3

(Réponse tardive, consultez ma note complémentaire).

La même règle s'applique lors de la tentative de création d'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 communautaire) car elle sert souvent à fermer les questions avec, où certaines personnes ont posté des questions similaires à celles-ci en essayant de lier une base de données et non une table et / ou une colonne.

Funk Forty Niner
la source
0

Une partie de moi se demande si vous pourriez fournir votre propre fonction de nettoyage personnalisée aussi simple que celle-ci:

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

Je n'y ai pas vraiment réfléchi, mais il semble que supprimer quoi que ce soit, à l'exception des caractères et des traits de soulignement, pourrait fonctionner.

Phil LaNasa
la source
1
Les noms de table MySQL peuvent contenir d'autres caractères. Voir dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil
@PhilLaNasa en fait certains défendent ce qu'ils devraient (référence du besoin). Étant donné que la plupart des SGBD ne respectent pas la casse en stockant le nom dans des caractères non différenciés, par exemple: MyLongTableNameil est facile de lire correctement, mais si vous vérifiez le nom stocké, il serait (probablement) MYLONGTABLENAMEqui n'est pas très lisible, donc MY_LONG_TABLE_NAMEest en fait plus lisible.
mloureiro
Il y a une très bonne raison de ne pas avoir ceci comme fonction: vous devriez très très rarement sélectionner un nom de table basé sur une entrée arbitraire. Vous ne voulez certainement pas qu'un utilisateur malveillant remplace "utilisateurs" ou "réservations" Select * From $table. Une liste blanche ou une correspondance de modèle stricte (par exemple, "noms commençant le rapport_ suivi de 1 à 3 chiffres uniquement") est vraiment essentielle ici.
IMSoP
0

Quant à la question principale de ce fil, les autres articles ont expliqué pourquoi nous ne pouvons pas lier 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, donc inutile de dire que copier-> coller ne fonctionnera pas. Adaptez-vous à vos besoins. Maintenant, cela peut ne pas fournir une sécurité à 100%, mais cela permet un certain contrôle sur les noms de colonnes lorsqu'ils "entrent" en tant que chaînes dynamiques et peuvent être modifiés du côté des utilisateurs. En outre, il n'est pas nécessaire de créer un tableau avec les noms et types de colonnes de votre table car ils sont extraits du schéma information_schema.

homme
la source