Czy instrukcje PHP PDO mogą zaakceptować nazwę tabeli lub kolumny jako parametr?

243

Dlaczego nie mogę przekazać nazwy tabeli do przygotowanej instrukcji PDO?

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

Czy istnieje inny bezpieczny sposób wstawienia nazwy tabeli do zapytania SQL? Z sejfem mam na myśli, że nie chcę tego robić

$sql = "SELECT * FROM $table WHERE 1"
Jrgns
źródło

Odpowiedzi:

212

Nazwy tabel i kolumn NIE MOGĄ zostać zastąpione parametrami w PDO.

W takim przypadku wystarczy po prostu ręcznie przefiltrować i zdezynfekować dane. Jednym ze sposobów jest przekazanie parametrów skróconych do funkcji, która wykona zapytanie dynamicznie, a następnie za pomocą switch()instrukcji utworzy białą listę prawidłowych wartości, które będą używane dla nazwy tabeli lub nazwy kolumny. W ten sposób żadne dane wejściowe użytkownika nigdy nie trafiają bezpośrednio do zapytania. Na przykład:

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

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

Nie pozostawiając domyślnej wielkości liter lub używając domyślnej wielkości liter, która zwraca komunikat o błędzie, upewniasz się, że zostaną użyte tylko te wartości, które chcesz użyć.

Noah Goodrich
źródło
17
+1 za opcje białej listy zamiast korzystania z jakiejkolwiek metody dynamicznej. Inną alternatywą może być mapowanie akceptowalnych nazw tabel na tablicę z kluczami, które odpowiadają potencjalnemu wprowadzeniu użytkownika (np. array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')Itp.)
Kzqai
4
Czytając o tym, przychodzi mi do głowy, że przykład tutaj generuje niepoprawny SQL dla złych danych wejściowych, ponieważ nie ma go default. Jeśli używasz tego wzorca, powinieneś albo oznaczyć jedną ze swoich liter casejako default, albo dodać wyraźny przypadek błędu, taki jakdefault: throw new InvalidArgumentException;
IMSoP
3
Myślałem prosty if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Dzięki za pomysł.
Phil Tune
2
Tęsknię mysql_real_escape_string(). Może tutaj mogę to powiedzieć bez kogoś, kto wskoczy i nie powie „Ale nie potrzebujesz tego z PDO”
Rolf,
Innym problemem jest to, że dynamiczne nazwy tabel psują kontrolę SQL.
Acyra
143

Aby zrozumieć, dlaczego wiązanie nazwy tabeli (lub kolumny) nie działa, musisz zrozumieć, jak działają symbole zastępcze w przygotowanych instrukcjach: nie są one po prostu podstawiane jako (odpowiednio poprzedzone) ciągi, a wynikowy SQL jest wykonywany. Zamiast tego DBMS, proszony o „przygotowanie” instrukcji, przedstawia kompletny plan zapytań, w jaki sposób wykona to zapytanie, w tym jakie tabele i indeksy zastosuje, które będą takie same bez względu na to, jak wypełnisz symbole zastępcze.

Plan SELECT name FROM my_table WHERE id = :valuebędzie taki sam, niezależnie od tego, co zastąpisz :value, ale pozornie podobnych SELECT name FROM :table WHERE id = :valuenie da się zaplanować, ponieważ DBMS nie ma pojęcia, z której tabeli wybierzesz.

To nie jest coś, co biblioteka abstrakcyjna, taka jak PDO, może lub powinna obejść, ponieważ zniweczyłaby 2 kluczowe cele przygotowanych instrukcji: 1) aby baza danych mogła z wyprzedzeniem zdecydować, w jaki sposób zostanie uruchomione zapytanie, i użyć tego samego planuj wiele razy; oraz 2) w celu uniknięcia problemów bezpieczeństwa poprzez oddzielenie logiki zapytania od zmiennej wejściowej.

IMSoP
źródło
1
To prawda, ale nie uwzględnia emulacji instrukcji PDO (która mogłaby sparametryzować identyfikatory obiektów SQL, choć nadal zgadzam się, że prawdopodobnie nie powinna).
eggyal
1
@eggyal Wydaje mi się, że emulacja ma na celu sprawienie, aby standardowa funkcjonalność działała na wszystkich odmianach DBMS, a nie dodawanie całkowicie nowej funkcjonalności. Symbol zastępczy dla identyfikatorów wymagałby również odrębnej składni nieobsługiwanej bezpośrednio przez żaden DBMS. PDO jest dość niskopoziomowym opakowaniem i na przykład nie oferuje generowania kodu SQL dla klauzul TOP/ LIMIT/ OFFSET, więc byłoby to trochę nie na miejscu jako funkcja.
IMSoP
13

Widzę, że to stary post, ale uznałem go za przydatny i pomyślałem, że podzielę się rozwiązaniem podobnym do sugerowanego przez @kzqai:

Mam funkcję, która odbiera dwa parametry, takie jak ...

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

Wewnątrz sprawdzam tablice, które skonfigurowałem, aby upewnić się, że dostępne są tylko tabele i kolumny z tabelami „błogosławionymi”:

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

Następnie sprawdzenie PHP przed uruchomieniem PDO wygląda jak ...

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
źródło
2
dobre na krótkie rozwiązanie, ale dlaczego nie tylko$pdo->query($sql)
jscripter
Głównie z przyzwyczajenia podczas przygotowywania zapytań, które muszą powiązać zmienną. Czytaj także powtarzane połączenia są szybsze w / wykonaj tutaj stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don
w twoim przykładzie nie ma powtarzających się połączeń
Your Common Sense
4

Korzystanie z tego pierwszego nie jest z natury bezpieczniejsze niż drugie, musisz oczyścić dane wejściowe, niezależnie od tego, czy są one częścią tablicy parametrów, czy prostej zmiennej. Więc nie widzę nic złego w korzystaniu z tego ostatniego formularza $table, pod warunkiem, że upewnisz się, że zawartość $tablejest bezpieczna (alphanum plus podkreślenia?) Przed użyciem.

Adam Bellaire
źródło
Biorąc pod uwagę, że pierwsza opcja nie zadziała, musisz użyć jakiejś formy dynamicznego budowania zapytań.
Noah Goodrich,
Tak, wspomniane pytanie nie zadziała. Próbowałem opisać, dlaczego próba zrobienia tego w ten sposób nie była szczególnie ważna.
Adam Bellaire,
3

(Późna odpowiedź, sprawdź moją notatkę dodatkową).

Ta sama zasada obowiązuje przy próbie utworzenia „bazy danych”.

Nie można użyć przygotowanej instrukcji do powiązania bazy danych.

To znaczy:

CREATE DATABASE IF NOT EXISTS :database

nie będzie działać. Zamiast tego użyj bezpiecznej listy.

Uwaga dodatkowa: dodałem tę odpowiedź (jako wiki społeczności), ponieważ często zamykała pytania, w których niektóre osoby zamieszczały podobne pytania, próbując powiązać bazę danych, a nie tabelę i / lub kolumnę.

Funk Forty Niner
źródło
0

Część mnie zastanawia się, czy możesz udostępnić własną niestandardową funkcję odkażania tak prostą:

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

Nie zastanawiałem się nad tym, ale wydaje się, że usunięcie czegokolwiek oprócz postaci i znaków podkreślenia może działać.

Phil LaNasa
źródło
1
Nazwy tabel MySQL mogą zawierać inne znaki. Zobacz dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil
@PhilLaNasa faktycznie niektórzy bronią, że powinni (potrzeba odniesienia). Ponieważ większość DBMS nie rozróżnia wielkich i małych liter, przechowując nazwę w niezróżnicowanych znakach, np .: MyLongTableNamełatwo ją poprawnie odczytać, ale jeśli sprawdzisz zapisaną nazwę, to (prawdopodobnie) MYLONGTABLENAMEnie będzie ona bardzo czytelna, więc MY_LONG_TABLE_NAMEjest bardziej czytelna.
mloureiro,
Jest bardzo dobry powód, aby nie używać tego jako funkcji: bardzo rzadko powinieneś wybierać nazwę tabeli na podstawie dowolnych danych wejściowych. Prawie na pewno nie chcesz, aby złośliwy użytkownik zastąpił „użytkowników” lub „rezerwacje” Select * From $table. Bardzo ważna jest tutaj biała lista lub ścisłe dopasowanie wzorca (np. „Nazwiska zaczynające się na raporcie_, a następnie tylko od 1 do 3 cyfr”).
IMSoP
0

Jeśli chodzi o główne pytanie w tym wątku, pozostałe posty wyjaśniły, dlaczego nie możemy wiązać wartości z nazwami kolumn podczas przygotowywania instrukcji, więc oto jedno rozwiązanie:

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

Powyższe to tylko przykład, więc nie trzeba dodawać, że kopiuj-> wklej nie zadziała. Dostosuj do swoich potrzeb. Teraz może to nie zapewniać 100% bezpieczeństwa, ale pozwala na pewną kontrolę nad nazwami kolumn, gdy „przychodzą” jako ciągi dynamiczne i mogą być zmieniane po stronie użytkownika. Ponadto nie ma potrzeby budowania tablicy z nazwami i typami kolumn tabeli, ponieważ są one wyodrębniane ze schematu_informacyjnego.

człowiek
źródło