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"

回答:


212

テーブル名と列名は、PDOのパラメーターで置き換えることはできません。

その場合は、単にデータを手動でフィルタリングしてサニタイズする必要があります。これを行う1つの方法は、クエリを動的に実行する関数に省略形のパラメーターを渡し、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。このパターンを使用している場合、あなたはどちらかあなたの1ラベルを付ける必要がありますcase秒としてdefault、またはのような明示的なエラーケースを追加default: throw new InvalidArgumentException;
IMSoP

3
シンプルだと思っていましたif ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }。アイデアをありがとう。
Phil Tune

2
寂しいmysql_real_escape_string()。多分ここに私は誰かが飛び込んで「PDOではそれを必要としない」と言わなくてもそれを言うことができます
Rolf

もう1つの問題は、動的テーブル名がSQL検査に違反することです。
Acyra

143

理解するために、なぜ表(または列)を結合名前が仕事をしない、あなたは準備された文の仕事にどのようにプレースホルダを理解する必要があります:彼らは単に(適切にエスケープ)文字列としてで置換されていない、そして得られたSQLを実行します。代わりに、ステートメントを「準備」するように要求されたDBMSは、どのテーブルとインデックスを使用するかを含む、そのクエリの実行方法に関する完全なクエリプランを作成します。これは、プレースホルダーの入力方法に関係なく同じです。

の計画はSELECT name FROM my_table WHERE id = :value、何を置き換えても同じになりますが、DBMSは実際にどのテーブルから選択するのかわからないため、:value一見似ているSELECT name FROM :table WHERE id = :valueように計画することはできません。

これは、PDOのような抽象ライブラリではなく、回避する必要があります。これは、準備されたステートメントの2つの主要な目的を無効にするためです。複数回計画する; 2)クエリのロジックを変数の入力から分離することにより、セキュリティの問題を防止する。


1
真実ですが、PDOのprepareステートメントのエミュレーションは考慮していません(おそらくSQLオブジェクト識別子をパラメーター化する可能性がありますが、私はまだそうすべきではないことに同意しています)。
eggyal

1
@eggyalエミュレーションは、完全に新しい機能を追加するのではなく、すべてのDBMSフレーバーで標準機能を機能させることを目的としていると思います。識別子のプレースホルダーには、DBMSで直接サポートされていない独自の構文も必要です。PDOは非常に低レベルのラッパーであり、たとえば、TOP/ LIMIT/ OFFSET句のSQL生成を提供しないため、これは機能としては少々場違いです。
IMSoP 2014年

13

これは古い投稿ですが、便利だと思い、@ kzqaiが提案したものと同様の解決策を共有したいと思いました。

私は2つのパラメータを受け取る関数を持っています...

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

内部では、「祝福された」テーブルを持つテーブルと列のみがアクセス可能であることを確認するために設定した配列に対してチェックします。

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

次に、PDOを実行する前のPHPチェックは次のようになります...

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
Don

あなたの例では繰り返される呼び出しはありません
あなたの常識

4

前者を使用することは、後者よりも本質的に安全ではないため、パラメーター配列の一部であっても、単純な変数であっても、入力を無害化する必要があります。ですから、後者のフォームをで使用しても問題はありません。使用する前に$tableのコンテンツ$tableが安全であることを確認してください(英数字とアンダースコア?)。


最初のオプションが機能しないことを考えると、動的クエリ構築の何らかの形式を使用する必要があります。
ノアグッドリッチ

はい、質問はそれが機能しないと述べました。そのようにしようとすることさえもそれほど重要ではなかった理由を説明しようとしていました。
Adam Bellaire、

3

(遅い答え、私のサイドノートを参照してください)。

「データベース」を作成しようとするときにも同じルールが適用されます。

準備済みステートメントを使用してデータベースをバインドすることはできません。

すなわち:

CREATE DATABASE IF NOT EXISTS :database

動作しないでしょう。代わりにセーフリストを使用してください。

サイドノート:私はこの回答を(コミュニティWikiとして)追加しました。これは、質問を閉じるためによく使用されていたためです。テーブルや列ではなくデータベースをバインドしようとして、これに似た質問を投稿した人もいます。


0

私の一部は、次のように簡単な独自のカスタム消毒機能を提供できるかどうか疑問に思っています。

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

私は本当にそれを熟考していませんが、文字とアンダースコア以外は何も削除しないとうまくいくようです。


1
MySQLテーブル名には他の文字を含めることができます。dev.mysql.com/doc/refman/5.0/en/identifiers.html
Phil

@PhilLaNasaは、実際にいくつかの彼らが必要(必要性の参照を)守ります。ほとんどのDBMSは、大文字と小文字を区別せず、区別されていない文字でMyLongTableName名前を格納します。たとえば、正しく読み取るのは簡単ですが、格納された名前を確認すると、(おそらく)MYLONGTABLENAMEあまり読みにくいためMY_LONG_TABLE_NAME、実際にはより読みやすくなります。
mloureiro 2015

これを関数として使用しないことには十分な理由があります。任意の入力に基づいてテーブル名を選択することはほとんどありません。悪意のあるユーザーが「ユーザー」または「予約」をに置き換えてほしくないことはほぼ間違いありませんSelect * From $table。ホワイトリストまたは厳密なパターンマッチ(たとえば、「report_で始まり、1から3桁のみが続く名前」)は、ここでは本当に重要です。
IMSoP 2017年

0

このスレッドの主な質問については、他の投稿で、ステートメントを準備するときになぜ値を列名にバインドできないのかが明らかになったので、1つの解決策を次に示します。

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から抽出されるため、テーブルの列名とタイプを含む配列を構築する必要はありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.