Чи можуть заяви PHO 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"

Відповіді:


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. Якщо ви користуєтеся цією схемою, вам слід позначити один із своїх cases defaultабо додати явний випадок помилки, такий якdefault: throw new InvalidArgumentException;
IMSoP

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, може або повинна обійтися, оскільки вона переможе дві основні цілі підготовлених операторів: 1) щоб база даних дозволила заздалегідь вирішити, як буде виконуватися запит, і використовувати той самий планувати кілька разів; та 2) запобігання проблемам безпеки, відокремлюючи логіку запиту від змінних входів.


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безпечний (літер плюс підкреслення?), Перед його використанням


Зважаючи на те, що перший варіант не працюватиме, ви повинні використовувати певну форму динамічного побудови запитів.
Ной Гудрич

Так, згадане питання не спрацювало. Я намагався описати, чому не було страшно важливо навіть намагатися зробити це саме так.
Адам Беллер

3

(Пізня відповідь, зверніться до моєї бічної записки).

Це ж правило діє і при спробі створення "бази даних".

Не можна використовувати підготовлений оператор для прив’язки бази даних.

Тобто:

CREATE DATABASE IF NOT EXISTS :database

не вийде. Використовуйте натомість сафеліст.

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


0

Частина мене замислюється, чи можна було б надати власну власну функцію дезінфекції такою простою, як ця:

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

Я насправді не замислювався над цим, але здається, що видалити що-небудь, крім символів і підкреслень, може спрацювати.


1
Імена таблиці MySQL можуть містити інші символи. Дивіться dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil

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

Наведене - лише приклад, тому зайве говорити, що копіювати> вставити не буде. Налаштуйте під свої потреби. Тепер це може не забезпечити 100% безпеку, але це дозволяє певний контроль над іменами стовпців, коли вони "входять" як динамічні рядки і можуть змінюватися на кінці користувачів. Крім того, не потрібно створювати деякий масив із іменами та типами стовпців таблиці, оскільки вони витягуються з інформаційної схеми.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.