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

243

Почему я не могу передать имя таблицы в подготовленный оператор 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"
Jrgns
источник

Ответы:

212

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

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

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

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

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

Ной Гудрич
источник
17
+1 для опций белого списка вместо использования какого-либо динамического метода. Другой альтернативой может быть сопоставление допустимых имен таблиц массиву с ключами, которые соответствуют потенциальному вводу пользователя (например, array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')и т. Д.)
Kzqai
4
Читая об этом, мне приходит в голову, что пример здесь генерирует недопустимый SQL для плохого ввода, потому что он не имеет default. Если вы используете этот шаблон, вы должны либо пометить один из своих casedefaultdefault: throw new InvalidArgumentException;
символов
3
Я думал просто if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Спасибо за идею.
Фил Тьюн
2
Я скучаю mysql_real_escape_string(). Может быть, здесь я могу сказать это без того, чтобы кто-то запрыгнул и сказал: «Но вам это не нужно с PDO»
Рольф
Другая проблема заключается в том, что имена динамических таблиц нарушают проверку SQL.
Acyra
143

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

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

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

IMSoP
источник
1
Верно, но не учитывает эмуляцию оператора подготовки PDO (которая могла бы параметризировать идентификаторы объектов SQL, хотя я все еще согласен, что это, вероятно, не должно).
eggyal
1
@eggyal Я полагаю, что эмуляция направлена ​​на то, чтобы стандартная функциональность работала на всех разновидностях СУБД, а не на добавление совершенно новой функциональности. Заполнителю для идентификаторов также потребуется отдельный синтаксис, не поддерживаемый напрямую СУБД. PDO - это низкоуровневая оболочка, которая, например, не предлагает и не генерирует SQL для предложений TOP/ LIMIT/ OFFSET, так что это было бы немного неуместно в качестве функции.
IMSoP
13

Я вижу, что это старый пост, но я нашел его полезным и решил поделиться решением, аналогичным предложенному @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);
}
Дон
источник
2
хорошо для краткого решения, но почему бы не просто$pdo->query($sql)
jscripter
В основном по привычке при подготовке запросов, которые должны связывать переменную. Также читать повторные вызовы быстрее с выполнением здесь stackoverflow.com/questions/4700623/pdos-query-vs-execute
Дон
в вашем примере нет повторяющихся звонков
Ваш здравый смысл
4

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

Адам Беллер
источник
Учитывая, что первый вариант не будет работать, вы должны использовать некоторую форму динамического построения запросов.
Ноа Гудрич
Да, упомянутый вопрос не сработает. Я пытался описать, почему не было так важно даже попытаться сделать это таким образом.
Адам Беллер
3

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

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

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

То есть:

CREATE DATABASE IF NOT EXISTS :database

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

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

Funk Forty Niner
источник
0

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

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

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

Фил ЛаНаса
источник
1
Имена таблиц MySQL могут содержать другие символы. См. Dev.mysql.com/doc/refman/5.0/en/identifiers.html
Фил,
@PhilLaNasa на самом деле некоторые защищать они должны (нужна ссылка). Поскольку большинство СУБД не учитывает регистр и хранит имя в недифференцированных символах, например: MyLongTableNameлегко читать правильно, но если вы проверите сохраненное имя, оно (вероятно) будет MYLONGTABLENAMEне очень читабельным, поэтому MY_LONG_TABLE_NAMEфактически более читабельным.
mloureiro
Есть очень веская причина не использовать это как функцию: вам очень редко следует выбирать имя таблицы на основе произвольного ввода. Вы почти наверняка не хотите, чтобы злонамеренный пользователь подставлял «пользователей» или «заказы» Select * From $table. Белый список или строгое соответствие шаблону (например, «имена, начинающиеся с отчета_, за которыми следуют только от 1 до 3 цифр») здесь действительно важно.
IMSoP
0

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

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.

человек
источник