As declarações PHP PDO podem aceitar o nome da tabela ou coluna como parâmetro?

243

Por que não posso passar o nome da tabela para uma declaração DOP preparada?

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

Existe outra maneira segura de inserir um nome de tabela em uma consulta SQL? Com seguro, quero dizer que não quero fazer

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

Respostas:

212

Os nomes de tabela e coluna NÃO PODEM ser substituídos por parâmetros no PDO.

Nesse caso, você simplesmente deseja filtrar e limpar os dados manualmente. Uma maneira de fazer isso é passar parâmetros abreviados para a função que executará a consulta dinamicamente e, em seguida, usar uma switch()instrução para criar uma lista branca de valores válidos a serem usados ​​para o nome da tabela ou o nome da coluna. Dessa forma, nenhuma entrada do usuário entra diretamente na consulta. Então, por exemplo:

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

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

Ao deixar nenhum caso padrão ou usar um caso padrão que retorna uma mensagem de erro, você garante que apenas os valores que você deseja que sejam usados ​​sejam usados.

Noah Goodrich
fonte
17
+1 nas opções da lista de permissões, em vez de usar qualquer tipo de método dinâmico. Outra alternativa pode ser o mapeamento de nomes de tabela aceitáveis para uma matriz com chaves que correspondem à entrada de potencial utilizador (por exemplo, array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')etc)
Kzqai
4
Lendo sobre isso, me ocorre que o exemplo aqui gera SQL inválido para entrada incorreta, porque não possui default. Se estiver usando esse padrão, você deve rotular um de seus cases como defaultou adicionar um caso de erro explícito, comodefault: throw new InvalidArgumentException;
IMSoP 22/10/2015
3
Eu estava pensando um simples if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Obrigado pela ideia.
Phil Tune
2
Eu sinto falta mysql_real_escape_string(). Talvez aqui eu possa dizê-lo sem que alguém entre e diga "Mas você não precisa disso com a DOP"
Rolf
A outra questão é que os nomes dinâmicos das tabelas interrompem a inspeção SQL.
Acyra 20/07/19
143

Para entender por que a ligação de um nome de tabela (ou coluna) não funciona, é necessário entender como funcionam os espaços reservados nas instruções preparadas: eles não são simplesmente substituídos em cadeias (como escapadas) e o SQL resultante é executado. Em vez disso, um DBMS solicitado a "preparar" uma instrução apresenta um plano de consulta completo sobre como executaria essa consulta, incluindo quais tabelas e índices ele usaria, quais serão os mesmos, independentemente de como você preenche os espaços reservados.

O plano para SELECT name FROM my_table WHERE id = :valueserá o mesmo, independentemente do que você substitua :value, mas o aparentemente semelhante SELECT name FROM :table WHERE id = :valuenão pode ser planejado, porque o DBMS não tem idéia de qual tabela você realmente selecionará.

Isso também não é algo que uma biblioteca de abstração como a DOP possa ou deva contornar, já que isso anularia os dois principais objetivos das instruções preparadas: 1) permitir que o banco de dados decida com antecedência como uma consulta será executada e use o mesmo planejar várias vezes; e 2) para evitar problemas de segurança, separando a lógica da consulta da entrada variável.

IMSoP
fonte
1
É verdade, mas não leva em conta a emulação de instrução de preparação do PDO (que pode parametrizar concebivelmente os identificadores de objeto SQL, embora eu ainda concorde que provavelmente não deveria).
eggyal 27/12/13
1
@eggyal Acho que a emulação tem como objetivo fazer com que a funcionalidade padrão funcione em todos os tipos de DBMS, em vez de adicionar uma funcionalidade completamente nova. Um espaço reservado para identificadores também precisaria de uma sintaxe distinta, não suportada diretamente por nenhum DBMS. O PDO é um invólucro de baixo nível e, por exemplo, não oferece e gera SQL para TOP/ LIMIT/ OFFSETcláusulas, portanto isso seria um pouco fora do lugar como um recurso.
IMSoP 01/01
13

Vejo que este é um post antigo, mas achei útil e pensei em compartilhar uma solução semelhante à sugerida pelo @kzqai:

Eu tenho uma função que recebe dois parâmetros como ...

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

No interior, verifico as matrizes que configurei para garantir que apenas tabelas e colunas com tabelas "abençoadas" sejam acessíveis:

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

Em seguida, a verificação do PHP antes de executar o DOP se parece com ...

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
fonte
2
bom para solução curta, mas porque não apenas$pdo->query($sql)
jscripter
Principalmente por hábito ao preparar consultas que precisam vincular uma variável. Leia também chamadas repetidas são mais rápidos w / executar aqui stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don
não há chamadas repetidas no seu exemplo
Seu senso comum
4

O uso do primeiro não é inerentemente mais seguro que o último, você precisa limpar a entrada, seja parte de uma matriz de parâmetros ou de uma variável simples. Portanto, não vejo nada de errado em usar o último formulário com $table, desde que você tenha certeza de que o conteúdo de $tableé seguro (alfanum mais sublinhados?) Antes de usá-lo.

Adam Bellaire
fonte
Considerando que a primeira opção não funcionará, é necessário usar alguma forma de criação de consultas dinâmicas.
Noah Goodrich
Sim, a pergunta mencionada não funcionará. Eu estava tentando descrever por que não era tão importante nem tentar fazer dessa maneira.
Adam Bellaire
3

(Resposta tardia, consulte minha nota lateral).

A mesma regra se aplica ao tentar criar um "banco de dados".

Você não pode usar uma instrução preparada para vincular um banco de dados.

Ou seja:

CREATE DATABASE IF NOT EXISTS :database

não funciona. Use uma lista segura.

Nota lateral: eu adicionei esta resposta (como um wiki da comunidade) porque costumava fechar perguntas, onde algumas pessoas postavam perguntas semelhantes a essa na tentativa de vincular um banco de dados e não uma tabela e / ou coluna.

Funk Quarenta Niner
fonte
0

Parte de mim se pergunta se você poderia fornecer sua própria função de desinfecção personalizada tão simples quanto isto:

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

Eu realmente não pensei nisso, mas parece que remover qualquer coisa, exceto caracteres e sublinhados, pode funcionar.

Phil LaNasa
fonte
1
Os nomes de tabelas do MySQL podem conter outros caracteres. Veja dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil
@PhilLaNasa, na verdade, alguns defendem que deveriam (referência da necessidade). Como a maioria dos DBMS não faz distinção entre maiúsculas e minúsculas, armazenando o nome em caracteres não diferenciados, por exemplo: MyLongTableNameé fácil ler corretamente, mas se você verificar o nome armazenado (provavelmente) provavelmente MYLONGTABLENAMEnão será muito legível, MY_LONG_TABLE_NAMEna verdade é mais legível.
Mloureiro
Há uma boa razão para não ter isso como uma função: você muito raramente deve selecionar um nome de tabela com base em entradas arbitrárias. Você quase certamente não deseja que um usuário mal-intencionado substitua "usuários" ou "reservas" Select * From $table. Uma lista de permissões ou correspondência estrita de padrões (por exemplo, "nomes que começam no relatório_ seguidos apenas de 1 a 3 dígitos") é realmente essencial aqui.
IMSoP 21/03/19
0

Quanto à questão principal deste segmento, os outros posts deixaram claro por que não podemos vincular valores a nomes de colunas ao preparar instruções, então aqui está uma solução:

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

O exemplo acima é apenas um exemplo, então nem é preciso dizer que copiar-> colar não funcionará. Ajuste para suas necessidades. Agora, isso pode não fornecer 100% de segurança, mas permite algum controle sobre os nomes das colunas quando elas "entram" como seqüências dinâmicas e podem ser alteradas no final do usuário. Além disso, não há necessidade de criar alguma matriz com os nomes e tipos de colunas da tabela, pois eles são extraídos do information_schema.

homem
fonte