¿Pueden las declaraciones PDO de PHP aceptar el nombre de la tabla o columna como parámetro?

243

¿Por qué no puedo pasar el nombre de la tabla a una declaración PDO preparada?

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

¿Hay otra forma segura de insertar un nombre de tabla en una consulta SQL? Con seguro, quiero decir que no quiero hacer

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

Respuestas:

212

Los nombres de tabla y columna NO PUEDEN reemplazarse por parámetros en PDO.

En ese caso, simplemente querrá filtrar y desinfectar los datos manualmente. Una forma de hacerlo es pasar parámetros abreviados a la función que ejecutará la consulta dinámicamente y luego usar una switch()instrucción para crear una lista blanca de valores válidos para el nombre de la tabla o columna. De esa forma, ninguna entrada del usuario va directamente a la consulta. Así por ejemplo:

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

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

Al no dejar ningún caso predeterminado o al usar un caso predeterminado que devuelve un mensaje de error, se asegura de que solo se usen los valores que desea usar.

Noah Goodrich
fuente
17
+1 para las opciones de la lista blanca en lugar de utilizar cualquier tipo de método dinámico. Otra alternativa podría ser el mapeo de nombres de tabla aceptables para una matriz con teclas que corresponden a la entrada potencial de usuario (por ejemplo, array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')etc.)
Kzqai
44
Al leer esto, se me ocurre que el ejemplo aquí genera SQL no válido para una entrada incorrecta, porque no tiene default. Si usa este patrón, debe etiquetar uno de sus casedefaultdefault: throw new InvalidArgumentException;
correos electrónicos
3
Estaba pensando en un simple if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Gracias por la idea
Phil Tune
2
Echo de menos mysql_real_escape_string(). Tal vez aquí puedo decirlo sin que alguien salte y diga "Pero no lo necesitas con DOP"
Rolf
El otro problema es que los nombres de tablas dinámicas interrumpen la inspección de SQL.
Acyra
143

Para entender por qué el enlace de un nombre de tabla (o columna) no funciona, debe comprender cómo funcionan los marcadores de posición en las declaraciones preparadas: no se sustituyen simplemente como cadenas (escapó adecuadamente) y se ejecuta el SQL resultante. En cambio, un DBMS al que se le pidió que "preparara" una declaración presenta un plan de consulta completo sobre cómo ejecutaría esa consulta, incluidas las tablas e índices que usaría, que serán las mismas independientemente de cómo complete los marcadores de posición.

El plan SELECT name FROM my_table WHERE id = :valueserá el mismo para lo que sea que sustituya :value, pero lo aparentemente similar SELECT name FROM :table WHERE id = :valueno se puede planificar, porque el DBMS no tiene idea de qué tabla realmente seleccionará.

Esto no es algo que una biblioteca de abstracción como PDO pueda o deba evitar, ya que anularía los 2 propósitos clave de las declaraciones preparadas: 1) permitir que la base de datos decida de antemano cómo se ejecutará una consulta y usar la misma planificar varias veces; y 2) para evitar problemas de seguridad separando la lógica de la consulta de la entrada variable.

IMSoP
fuente
1
Es cierto, pero no tiene en cuenta la emulación de declaración de preparación de PDO (que posiblemente podría parametrizar identificadores de objetos SQL, aunque todavía estoy de acuerdo en que probablemente no debería).
eggyal
1
@eggyal Supongo que la emulación apunta a hacer que la funcionalidad estándar funcione en todos los sabores de DBMS, en lugar de agregar una funcionalidad completamente nueva. Un marcador de posición para identificadores también necesitaría una sintaxis distinta que ningún DBMS admite directamente. PDO es un envoltorio de bajo nivel y, por ejemplo, no ofrece una generación de SQL para cláusulas TOP/ LIMIT/ OFFSET, por lo que esto sería un poco fuera de lugar como característica.
IMSoP 01 de
13

Veo que esta es una publicación antigua, pero la encontré útil y pensé en compartir una solución similar a lo que sugirió @kzqai:

Tengo una función que recibe dos parámetros como ...

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

En el interior, compruebo las matrices que configuré para asegurarme de que solo las tablas y columnas con tablas "bendecidas" sean accesibles:

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

Entonces la comprobación de PHP antes de ejecutar PDO se ve como ...

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
fuente
2
bueno para una solución corta, pero ¿por qué no solo?$pdo->query($sql)
jscripter
Principalmente por costumbre al preparar consultas que tienen que vincular una variable. También lea las llamadas repetidas son más rápidas con ejecutar aquí stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don
no hay llamadas repetidas en su ejemplo
su sentido común
4

Usar el primero no es inherentemente más seguro que el segundo, debe desinfectar la entrada, ya sea parte de una matriz de parámetros o una variable simple. Por lo tanto, no veo nada de malo en usar este último formulario $table, siempre y cuando se asegure de que el contenido $tablees seguro (¿alfanum más guiones bajos?) Antes de usarlo.

Adam Bellaire
fuente
Teniendo en cuenta que la primera opción no funcionará, debe usar alguna forma de creación dinámica de consultas.
Noah Goodrich
Sí, la pregunta mencionó que no funcionará. Estaba tratando de describir por qué no era tan importante tratar de hacerlo de esa manera.
Adam Bellaire
3

(Respuesta tardía, consulte mi nota al margen).

La misma regla se aplica cuando se intenta crear una "base de datos".

No puede usar una declaración preparada para vincular una base de datos.

Es decir:

CREATE DATABASE IF NOT EXISTS :database

no trabajará. Use una lista segura en su lugar.

Nota al margen: agregué esta respuesta (como wiki de la comunidad) porque a menudo solía cerrar preguntas, donde algunas personas publicaban preguntas similares a esta al tratar de vincular una base de datos y no una tabla y / o columna.

Funk Forty Niner
fuente
0

Parte de mí se pregunta si podría proporcionar su propia función de desinfección personalizada tan simple como esta:

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

Realmente no lo he pensado, pero parece que eliminar todo, excepto los caracteres y los guiones bajos, podría funcionar.

Phil LaNasa
fuente
1
Los nombres de la tabla MySQL pueden contener otros caracteres. Ver dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil
@PhilLaNasa en realidad algunos defienden que deberían (necesita referencia). Dado que la mayoría de los DBMS no distinguen entre mayúsculas y minúsculas y almacenan el nombre en caracteres no diferenciados, por ejemplo: MyLongTableNamees fácil de leer correctamente, pero si verifica el nombre almacenado sería (probablemente) el MYLONGTABLENAMEque no es muy legible, por MY_LONG_TABLE_NAMElo que en realidad es más legible.
mloureiro
Hay una muy buena razón para no tener esto como una función: muy rara vez debe seleccionar un nombre de tabla basado en una entrada arbitraria. Es casi seguro que no desea que un usuario malintencionado sustituya a "usuarios" o "reservas" Select * From $table. Una lista blanca o una coincidencia de patrón estricta (por ejemplo, "nombres que comienzan el informe_ seguidos de 1 a 3 dígitos solamente") realmente es esencial aquí.
IMSoP
0

En cuanto a la pregunta principal en este hilo, las otras publicaciones dejaron en claro por qué no podemos vincular valores a los nombres de columna al preparar declaraciones, por lo que aquí hay una solución:

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

Lo anterior es solo un ejemplo, por lo que no hace falta decir que copiar-> pegar no funcionará. Ajústate a tus necesidades. Ahora, esto puede no proporcionar un 100% de seguridad, pero permite cierto control sobre los nombres de columna cuando "entran" como cadenas dinámicas y pueden modificarse en los usuarios finales. Además, no es necesario crear una matriz con los nombres y tipos de columnas de la tabla, ya que se extraen del esquema de información.

hombre
fuente