Können PHP PDO-Anweisungen den Tabellen- oder Spaltennamen als Parameter akzeptieren?


243

Warum kann ich den Tabellennamen nicht an eine vorbereitete PDO-Anweisung übergeben?

$stmt = $dbh->prepare('SELECT * FROM :table WHERE 1');
if ($stmt->execute(array(':table' => 'users'))) {
    var_dump($stmt->fetchAll());
}

Gibt es eine andere sichere Möglichkeit, einen Tabellennamen in eine SQL-Abfrage einzufügen? Mit Safe meine ich, dass ich das nicht tun will

$sql = "SELECT * FROM $table WHERE 1"

Antworten:


212

Tabellen- und Spaltennamen können in PDO NICHT durch Parameter ersetzt werden.

In diesem Fall möchten Sie die Daten einfach manuell filtern und bereinigen. Eine Möglichkeit, dies zu tun, besteht darin, Kurzparameter an die Funktion zu übergeben, die die Abfrage dynamisch ausführt, und dann mithilfe einer switch()Anweisung eine weiße Liste gültiger Werte zu erstellen, die für den Tabellennamen oder den Spaltennamen verwendet werden sollen. Auf diese Weise wird keine Benutzereingabe direkt in die Abfrage eingegeben. Also zum Beispiel:

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

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

Indem Sie keinen Standardfall belassen oder einen Standardfall verwenden, der eine Fehlermeldung zurückgibt, stellen Sie sicher, dass nur Werte verwendet werden, die Sie verwenden möchten.


17
+1 für Whitelist-Optionen anstelle einer dynamischen Methode. Eine andere Alternative könnte darin bestehen, akzeptable Tabellennamen einem Array mit Schlüsseln array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')
zuzuordnen

4
Beim Lesen fällt mir ein, dass das Beispiel hier ungültiges SQL für fehlerhafte Eingaben generiert, weil es keine hat default. Wenn dieses Muster verwenden, sollten Sie entweder einen Ihrer beschriften cases als default, oder fügen Sie eine explizite Fehlerfall wiedefault: throw new InvalidArgumentException;
IMSoP

3
Ich dachte ein einfaches if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Danke für die Idee.
Phil Tune

2
Ich vermisse mysql_real_escape_string(). Vielleicht kann ich es hier sagen, ohne dass jemand
Rolf

Das andere Problem ist, dass dynamische Tabellennamen die SQL-Überprüfung unterbrechen.
Acyra

143

Um zu verstehen, warum das Binden eines Tabellen- (oder Spalten-) Namens nicht funktioniert, müssen Sie verstehen, wie die Platzhalter in vorbereiteten Anweisungen funktionieren: Sie werden nicht einfach durch (entsprechend maskierte) Zeichenfolgen ersetzt und die resultierende SQL ausgeführt. Stattdessen erstellt ein DBMS, das aufgefordert wird, eine Anweisung "vorzubereiten", einen vollständigen Abfrageplan für die Ausführung dieser Abfrage, einschließlich der verwendeten Tabellen und Indizes, die unabhängig davon, wie Sie die Platzhalter ausfüllen, identisch sind.

Der Plan für SELECT name FROM my_table WHERE id = :valueist derselbe, den Sie ersetzen :value, aber der scheinbar ähnliche Plan SELECT name FROM :table WHERE id = :valuekann nicht geplant werden, da das DBMS keine Ahnung hat, aus welcher Tabelle Sie tatsächlich auswählen werden.

Dies kann oder sollte auch keine Abstraktionsbibliothek wie PDO umgehen, da dies die beiden Hauptziele vorbereiteter Anweisungen zunichte machen würde: 1) Damit die Datenbank im Voraus entscheiden kann, wie eine Abfrage ausgeführt wird, und diese verwenden kann mehrmals planen; und 2) um Sicherheitsprobleme zu verhindern, indem die Logik der Abfrage von der Variableneingabe getrennt wird.


1
Richtig, berücksichtigt jedoch nicht die Emulation der vorbereitenden Anweisungen von PDO (die möglicherweise SQL-Objektkennungen parametrisieren könnte , obwohl ich immer noch der Meinung bin, dass dies wahrscheinlich nicht der Fall sein sollte).
Eggyal

1
@eggyal Ich denke, die Emulation zielt darauf ab, dass die Standardfunktionalität für alle DBMS-Varianten funktioniert, anstatt völlig neue Funktionen hinzuzufügen. Ein Platzhalter für Bezeichner würde auch eine eindeutige Syntax benötigen, die von keinem DBMS direkt unterstützt wird. PDO ist ein ziemlich einfacher Wrapper und bietet beispielsweise keine SQL-Generierung für TOP/ LIMIT/ OFFSET-Klauseln an, sodass dies als Feature etwas fehl am Platz wäre.
IMSoP

13

Ich sehe, dass dies ein alter Beitrag ist, aber ich fand ihn nützlich und dachte, ich würde eine Lösung teilen, die der von @kzqai vorgeschlagenen ähnelt:

Ich habe eine Funktion, die zwei Parameter empfängt wie ...

function getTableInfo($inTableName, $inColumnName) {
    ....
}

Im Inneren überprüfe ich anhand von Arrays, die ich eingerichtet habe, um sicherzustellen, dass nur Tabellen und Spalten mit "gesegneten" Tabellen zugänglich sind:

$allowed_tables_array = array('tblTheTable');
$allowed_columns_array['tblTheTable'] = array('the_col_to_check');

Dann sieht die PHP-Prüfung vor dem Ausführen von PDO wie folgt aus ...

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
gut für kurze Lösung, aber warum nicht einfach$pdo->query($sql)
jscripter

Meist aus Gewohnheit bei der Vorbereitung von Abfragen, die eine Variable binden müssen. Auch gelesene wiederholte Anrufe sind schneller w / hier ausführen stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don

In Ihrem Beispiel gibt es keine wiederholten Anrufe
Ihr gesunder Menschenverstand

4

Die Verwendung der ersteren ist von Natur aus nicht sicherer als die der letzteren. Sie müssen die Eingabe bereinigen, unabhängig davon, ob sie Teil eines Parameterarrays oder einer einfachen Variablen ist. Ich sehe also nichts Falsches daran, das letztere Formular mit zu verwenden $table, vorausgesetzt, Sie stellen sicher, dass der Inhalt von $tablesicher ist (Alphanum plus Unterstriche?), Bevor Sie es verwenden.


Da die erste Option nicht funktioniert, müssen Sie eine Form der dynamischen Abfrageerstellung verwenden.
Noah Goodrich

Ja, die erwähnte Frage wird nicht funktionieren. Ich versuchte zu beschreiben, warum es nicht besonders wichtig war, es überhaupt so zu machen.
Adam Bellaire

3

(Späte Antwort, konsultieren Sie meine Randnotiz).

Die gleiche Regel gilt beim Versuch, eine "Datenbank" zu erstellen.

Sie können keine vorbereitete Anweisung zum Binden einer Datenbank verwenden.

Dh:

CREATE DATABASE IF NOT EXISTS :database

wird nicht funktionieren. Verwenden Sie stattdessen eine Sicherheitsliste.

Randnotiz: Ich habe diese Antwort hinzugefügt (als Community-Wiki), weil sie häufig zum Schließen von Fragen verwendet wurde, bei denen einige Personen ähnliche Fragen stellten, als sie versuchten, eine Datenbank und keine Tabelle und / oder Spalte zu binden .


0

Ein Teil von mir fragt sich, ob Sie so einfach Ihre eigene Desinfektionsfunktion bereitstellen könnten:

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

Ich habe nicht wirklich darüber nachgedacht, aber es scheint, als würde alles entfernt, außer Zeichen und Unterstrichen.


1
MySQL-Tabellennamen können andere Zeichen enthalten. Siehe dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil

@PhilLaNasa eigentlich einige verteidigen sie sollten (brauchen Referenz). Da bei den meisten DBMS die Groß- und Kleinschreibung nicht berücksichtigt wird, wird der Name in nicht differenzierten Zeichen MyLongTableNamegespeichert. Beispiel : Es ist leicht, richtig zu lesen. Wenn Sie jedoch den gespeicherten Namen überprüfen, ist dies (wahrscheinlich) MYLONGTABLENAMEnicht sehr lesbar und daher MY_LONG_TABLE_NAMEbesser lesbar.
mloureiro

Es gibt einen sehr guten Grund, dies nicht als Funktion zu haben: Sie sollten sehr selten einen Tabellennamen auswählen, der auf willkürlichen Eingaben basiert. Sie möchten mit ziemlicher Sicherheit nicht, dass ein böswilliger Benutzer "Benutzer" oder "Buchungen" ersetzt Select * From $table. Eine Whitelist oder eine strikte Musterübereinstimmung (z. B. "Namen, die mit report_ beginnen, gefolgt von nur 1 bis 3 Ziffern") sind hier wirklich wichtig.
IMSoP

0

Was die Hauptfrage in diesem Thread betrifft, haben die anderen Beiträge deutlich gemacht, warum wir bei der Vorbereitung von Anweisungen keine Werte an Spaltennamen binden können. Hier ist eine Lösung:

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));

Das Obige ist nur ein Beispiel, daher funktioniert Kopieren-> Einfügen natürlich nicht. Passen Sie es an Ihre Bedürfnisse an. Dies bietet möglicherweise keine 100% ige Sicherheit, ermöglicht jedoch eine gewisse Kontrolle über die Spaltennamen, wenn diese als dynamische Zeichenfolgen "eingehen" und auf Benutzerseite geändert werden können. Darüber hinaus ist es nicht erforderlich, ein Array mit den Namen und Typen Ihrer Tabellenspalten zu erstellen, da diese aus dem Informationsschema extrahiert werden.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.