Le istruzioni PHP DOP possono accettare il nome di tabella o colonna come parametro?


243

Perché non riesco a passare il nome della tabella a un'istruzione PDO preparata?

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

Esiste un altro modo sicuro per inserire un nome di tabella in una query SQL? Con la sicurezza, intendo che non voglio fare

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

Risposte:


212

I nomi di tabella e colonna NON POSSONO essere sostituiti da parametri in DOP.

In tal caso, dovrai semplicemente filtrare e disinfettare i dati manualmente. Un modo per eseguire questa operazione consiste nel passare parametri di stenografia alla funzione che eseguirà la query in modo dinamico e quindi utilizzare switch()un'istruzione per creare una lista bianca di valori validi da utilizzare per il nome della tabella o della colonna. In questo modo nessun input dell'utente viene mai inserito direttamente nella query. Quindi per esempio:

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

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

Non lasciando alcun caso predefinito o utilizzando un caso predefinito che restituisce un messaggio di errore, si garantisce che vengano utilizzati solo i valori che si desidera utilizzare.


17
+1 per le opzioni di whitelisting invece di utilizzare qualsiasi tipo di metodo dinamico. Un'altra alternativa potrebbe essere la mappatura di nomi di tabella accettabili su un array con chiavi che corrispondono al potenziale input dell'utente (ad es array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data').
Ecc

4
Leggendo questo, mi viene in mente che l'esempio qui genera SQL non valido per input errato, perché non ha default. Se si utilizza questo modello, è necessario etichettare uno dei propri casecome defaulto aggiungere un caso di errore esplicito comedefault: throw new InvalidArgumentException;
IMSoP

3
Stavo pensando un semplice if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Grazie per l'idea
Phil Tune,

2
Mi manca mysql_real_escape_string(). Forse qui posso dirlo senza che qualcuno salti dentro e dica "Ma non ne hai bisogno con il DOP"
Rolf

L'altro problema è che i nomi di tabella dinamici interrompono l'ispezione SQL.
Acyra,

143

Per capire perché l' associazione di un nome di tabella (o colonna) non funziona, devi capire come funzionano i segnaposto nelle istruzioni preparate: non vengono semplicemente sostituiti in stringhe (opportunamente sfuggite) e l'SQL risultante viene eseguito. Invece, un DBMS ha chiesto di "preparare" un'istruzione che presenta un piano di query completo su come eseguire quella query, inclusi quali tabelle e indici userebbe, che sarà lo stesso indipendentemente da come si riempiono i segnaposto.

Il piano per SELECT name FROM my_table WHERE id = :valuesarà lo stesso per qualunque cosa tu sostituisca :value, ma l'apparente simile SELECT name FROM :table WHERE id = :valuenon può essere pianificato, perché il DBMS non ha idea di quale tabella scegliere.

Questo non è qualcosa che una libreria di astrazione come PDO può o dovrebbe aggirare, dal momento che annullerebbe i 2 scopi chiave delle istruzioni preparate: 1) per consentire al database di decidere in anticipo come verrà eseguita una query e utilizzare lo stesso pianificare più volte; e 2) per prevenire problemi di sicurezza separando la logica della query dall'input variabile.


1
È vero, ma non tiene conto dell'emulazione dell'istruzione preparazione PDO (che potrebbe concepibilmente parametrizzare gli identificatori di oggetti SQL, anche se sono ancora d'accordo sul fatto che probabilmente non dovrebbe).
Eggyal

1
@eggyal Immagino che l'emulazione abbia lo scopo di far funzionare la funzionalità standard su tutti i gusti DBMS, piuttosto che aggiungere funzionalità completamente nuove. Un segnaposto per gli identificatori avrebbe anche bisogno di una sintassi distinta non direttamente supportata da nessun DBMS. PDO è piuttosto un wrapper di basso livello e, ad esempio, non offre e genera SQL per le clausole TOP/ LIMIT/ OFFSET, quindi questo sarebbe un po 'fuori posto come funzionalità.
IMSoP

13

Vedo che questo è un vecchio post, ma l'ho trovato utile e ho pensato di condividere una soluzione simile a quanto suggerito da @kzqai:

Ho una funzione che riceve due parametri come ...

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

All'interno controllo contro gli array che ho impostato per assicurarmi che solo le tabelle e le colonne con tabelle "benedette" siano accessibili:

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

Quindi il controllo PHP prima di eseguire DOP sembra ...

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
buono per una soluzione breve, ma perché non solo$pdo->query($sql)
jscripter il

Principalmente per abitudine durante la preparazione di query che devono associare una variabile. Leggi anche le chiamate ripetute sono più veloci con / esegui qui stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don

non ci sono chiamate ripetute nel tuo esempio
Il tuo senso comune

4

L'uso del primo non è intrinsecamente più sicuro del secondo, è necessario disinfettare l'input che faccia parte di un array di parametri o di una semplice variabile. Quindi non vedo nulla di sbagliato nell'uso di quest'ultimo modulo $table, purché tu ti assicuri che il contenuto di $tablesia sicuro (alfanum plus underscore?) Prima di usarlo.


Considerando che la prima opzione non funziona, è necessario utilizzare una qualche forma di creazione dinamica delle query.
Noah Goodrich,

Sì, la domanda menzionata non funzionerà. Stavo cercando di descrivere perché non era terribilmente importante nemmeno provare a farlo in quel modo.
Adam Bellaire,

3

(Risposta in ritardo, consultare la mia nota a margine).

La stessa regola si applica quando si tenta di creare un "database".

Non è possibile utilizzare un'istruzione preparata per associare un database.

Vale a dire:

CREATE DATABASE IF NOT EXISTS :database

non funzionerà. Utilizzare invece una lista di sicurezza.

Nota a margine: ho aggiunto questa risposta (come wiki della comunità) perché spesso veniva utilizzata per chiudere le domande, in cui alcune persone hanno pubblicato domande simili a questa nel tentativo di associare un database e non una tabella e / o una colonna.


0

Una parte di me si chiede se potresti fornire la tua funzione di sanificazione personalizzata semplice come questa:

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

Non ci ho ancora pensato, ma sembra che rimuovere qualsiasi cosa tranne caratteri e caratteri di sottolineatura potrebbe funzionare.


1
I nomi delle tabelle MySQL possono contenere altri caratteri. Vedi dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil

@PhilLaNasa in realtà alcuni difendono che dovrebbero (bisogno di riferimento). Poiché la maggior parte dei DBMS non distingue tra maiuscole e minuscole, memorizza il nome in caratteri non differenziati, ad esempio: MyLongTableNameè facile da leggere, ma se si controlla il nome memorizzato sarebbe (probabilmente) MYLONGTABLENAMEche non è molto leggibile, quindi in MY_LONG_TABLE_NAMErealtà è più leggibile.
mloureiro,

C'è un ottimo motivo per non avere questo come funzione: molto raramente dovresti selezionare un nome di tabella basato su input arbitrari. Quasi certamente non si desidera che un utente malintenzionato sostituisca "utenti" o "prenotazioni" Select * From $table. Una whitelist o una corrispondenza rigida del modello (ad es. "Nomi che iniziano il rapporto_ seguito solo da 1 a 3 cifre") è davvero essenziale qui.
IMSoP

0

Per quanto riguarda la domanda principale in questo thread, gli altri post hanno chiarito perché non possiamo associare i valori ai nomi delle colonne durante la preparazione delle istruzioni, quindi ecco una soluzione:

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

Quanto sopra è solo un esempio, quindi inutile dirlo, copia-> incolla non funzionerà. Adatta alle tue esigenze. Ora, questo potrebbe non fornire il 100% di sicurezza, ma consente un certo controllo sui nomi delle colonne quando "entrano" come stringhe dinamiche e possono essere modificati sul lato degli utenti. Inoltre, non è necessario creare alcun array con i nomi e i tipi delle colonne della tabella poiché sono estratti da information_schema.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.