Могут ли выражения PHP PDO принимать имя таблицы или столбца в качестве параметра?

Почему я не могу передать имя таблицы в подготовленный оператор PDO?

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

Есть ли другой безопасный способ вставить имя таблицы в запрос SQL? С безопасным, я имею в виду, что я не хочу делать

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

Ответ 1

Имена таблиц и столбцов НЕ МОГУТ заменяться параметрами в PDO.

В этом случае вы просто захотите отфильтровать и очистить данные вручную. Один из способов сделать это - передать сокращенные параметры в функцию, которая будет динамически выполнять запрос, а затем использовать инструкцию switch() для создания белого списка допустимых значений, которые будут использоваться для имени таблицы или имени столбца. Таким образом, никакой пользовательский ввод никогда не входит непосредственно в запрос. Так, например:

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

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

Не оставляя регистр по умолчанию или используя регистр по умолчанию, который возвращает сообщение об ошибке, вы гарантируете, что используются только те значения, которые вы хотите использовать.

Ответ 2

Чтобы понять, почему привязка имени таблицы (или столбца) не работает, вы должны понять, как работают заполнители в подготовленных операциях: они не просто заменяются как строки (соответственно экранированные), а результирующий SQL выполняется. Вместо этого СУБД, попросив "подготовить" выражение, содержит полный план запросов о том, как он будет выполнять этот запрос, включая те таблицы и индексы, которые он будет использовать, которые будут одинаковыми независимо от того, как вы заполняете заполнители.

План SELECT name FROM my_table WHERE id = :value будет тем же самым, что вы замените :value, но похожее подобное SELECT name FROM :table WHERE id = :value невозможно спланировать, потому что СУБД не имеет понятия, какую таблицу вы собираетесь выбрать.

Это не то, что библиотека абстракции, такая как PDO, может или должна работать, так как она победит две ключевые цели подготовленных операторов: 1) позволить базе данных заранее решить, как будет выполняться запрос, и использовать один и тот же план несколько раз; и 2) для предотвращения проблем безопасности, отделяя логику запроса от ввода переменной.

Ответ 3

Я вижу, что это старый пост, но я счел его полезным и подумал, что поделился бы решением, аналогичным тому, что предложил @kzqai:

У меня есть функция, которая получает два параметра, например...

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

Внутри я проверяю массивы, которые я настроил, чтобы убедиться, что доступны только таблицы и столбцы с "блаженными" таблицами:

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

Затем проверка PHP перед запуском PDO выглядит как...

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

Ответ 4

Использование первого не является по своей сути более безопасным, чем последнее, вам нужно дезинфицировать ввод, будь то часть массива параметров или простая переменная. Поэтому я не вижу ничего плохого в использовании последней формы с $table, если вы убедитесь, что содержимое $table безопасно (alphanum plus underscores?), Прежде чем использовать его.

Ответ 5

(Поздний ответ, обратитесь к моей записке).

То же правило применяется при попытке создать "базу данных".

Вы не можете использовать подготовленный оператор для привязки базы данных.

То есть:

CREATE DATABASE IF NOT EXISTS :database

не будет работать. Вместо этого используйте список безопасности.

Примечание: я добавил этот ответ (как вики сообщества), потому что он часто использовался для закрытия вопросов, где некоторые люди публиковали вопросы, подобные этим, пытаясь связать базу данных, а не таблицу и/или столбец.

Ответ 6

Часть меня удивляется, если вы могли бы предоставить свою собственную функцию санирования так же просто, как это:

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

Я действительно не продумал это, но кажется, что он удаляет все, кроме символов, и подчеркивания могут работать.

Ответ 7

Что касается основного вопроса в этом потоке, другие сообщения дали понять, почему мы не можем привязывать значения к именам столбцов при подготовке операторов, так что вот одно из решений:

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

Вышеприведенное является просто примером, поэтому, разумеется, copy- > paste не будет работать. Отрегулируйте для ваших потребностей. Теперь это может не обеспечить 100% -ную защиту, но позволяет контролировать имена столбцов, когда они "входят" как динамические строки и могут быть изменены в конце пользователя. Кроме того, нет необходимости создавать какой-либо массив с именами и типами столбцов таблицы, поскольку они извлекаются из information_schema.