PHP PDO Statement สามารถรับชื่อตารางหรือคอลัมน์เป็นพารามิเตอร์ได้หรือไม่?


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ที่ไม่ถูกต้องสำหรับการป้อนข้อมูลที่ไม่ดีเพราะมันไม่มี หากใช้รูปแบบนี้คุณควรติดป้ายกำกับรายการใดรายการหนึ่ง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

ปัญหาอื่น ๆ คือชื่อตารางแบบไดนามิกทำลายการตรวจสอบ SQL
Acyra

143

เพื่อให้เข้าใจว่าเหตุใดการผูกชื่อตาราง (หรือคอลัมน์) จึงไม่ทำงานคุณต้องเข้าใจว่าตัวยึดตำแหน่งในคำสั่งที่เตรียมไว้ทำงานอย่างไร: พวกมันไม่ได้ถูกแทนที่ด้วยสตริง (อย่างเหมาะสม) และ SQL ที่เกิดขึ้น แต่ DBMS ขอให้ "เตรียม" คำสั่งที่มาพร้อมกับแผนแบบสอบถามที่สมบูรณ์สำหรับวิธีที่จะดำเนินการแบบสอบถามซึ่งรวมถึงตารางและดัชนีที่จะใช้ซึ่งจะเหมือนกันโดยไม่คำนึงถึงวิธีการที่คุณกรอกตัวยึดตำแหน่ง

แผนสำหรับSELECT name FROM my_table WHERE id = :valueจะเหมือนกันทุกอย่างที่คุณแทนที่:valueแต่ดูเหมือนSELECT name FROM :table WHERE id = :valueไม่สามารถวางแผนได้เพราะ DBMS ไม่มีความคิดว่าคุณจะเลือกตารางจริง ๆ

นี่ไม่ใช่สิ่งที่ห้องสมุดนามธรรมเช่น PDO สามารถหรือควรหลีกเลี่ยงเพราะจะเอาชนะ 2 จุดประสงค์หลักของข้อความที่เตรียมไว้: 1) เพื่อให้ฐานข้อมูลตัดสินใจล่วงหน้าว่าจะเรียกใช้แบบสอบถามอย่างไรและใช้แบบเดียวกัน วางแผนหลาย ๆ ครั้ง และ 2) เพื่อป้องกันปัญหาด้านความปลอดภัยโดยการแยกลอจิกของแบบสอบถามจากอินพุตตัวแปร


1
จริง แต่ไม่บัญชีสำหรับการจำลองการเตรียมคำสั่งของ PDO (ซึ่งอาจทำให้พารามิเตอร์ตัวระบุวัตถุ SQL เป็นไปได้ถึงแม้ว่าฉันยังเห็นด้วยว่าอาจไม่ควร)
eggyal

1
@eggyal ฉันเดาว่าการเลียนแบบมีวัตถุประสงค์เพื่อให้ฟังก์ชันการทำงานมาตรฐานทำงานได้กับทุกรสชาติของ DBMS แทนที่จะเพิ่มฟังก์ชันการทำงานใหม่ทั้งหมด ตัวยึดตำแหน่งสำหรับตัวระบุจะต้องใช้ไวยากรณ์ที่แตกต่างซึ่งไม่ได้รับการสนับสนุนโดยตรงจาก DBMS ใด ๆ PDO นั้นเป็น wrapper ระดับต่ำและไม่ได้มีไว้สำหรับการเสนอและการสร้าง SQL สำหรับTOP/ LIMIT/ OFFSETclauses ดังนั้นมันจึงค่อนข้างแปลกที่จะใช้เป็นฟีเจอร์
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
Don

ในตัวอย่างของคุณไม่มีการโทรซ้ำ ๆ
สามัญสำนึกของคุณ

4

การใช้เก่าไม่ปลอดภัยกว่าหลังคุณต้อง sanitize อินพุตไม่ว่าจะเป็นส่วนหนึ่งของอาร์เรย์พารามิเตอร์หรือตัวแปรง่าย ดังนั้นฉันจึงไม่เห็นสิ่งผิดปกติในการใช้แบบฟอร์มหลังด้วย$tableหากคุณตรวจสอบให้แน่ใจว่าเนื้อหาของ$tableนั้นปลอดภัย (alphanum plus underscores?) ก่อนใช้งาน


เมื่อพิจารณาว่าตัวเลือกแรกใช้งานไม่ได้คุณต้องใช้การสร้างคิวรีแบบไดนามิกบางรูปแบบ
Noah Goodrich

ใช่คำถามที่กล่าวถึงจะไม่ทำงาน ฉันพยายามอธิบายว่าทำไมจึงไม่สำคัญอย่างยิ่งที่จะลองทำอย่างนั้น
Adam Bellaire

3

(ตอบรับช้าปรึกษาข้อความด้านข้างของฉัน)

ใช้กฎเดียวกันนี้เมื่อพยายามสร้าง "ฐานข้อมูล"

คุณไม่สามารถใช้คำสั่งที่เตรียมไว้เพื่อผูกฐานข้อมูล

เช่น:

CREATE DATABASE IF NOT EXISTS :database

จะไม่ทำงาน. ใช้ safelist แทน

หมายเหตุด้านข้าง:ฉันเพิ่มคำตอบนี้ (เป็นวิกิชุมชน) เพราะมักใช้เพื่อปิดคำถามด้วยซึ่งบางคนโพสต์คำถามที่คล้ายกันนี้ในความพยายามผูกฐานข้อมูลไม่ใช่ตารางและ / หรือคอลัมน์


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

มีเหตุผลที่ดีมากที่จะไม่ใช้สิ่งนี้เป็นฟังก์ชั่น: คุณควรเลือกชื่อตารางตามอินพุตโดยพลการน้อยมาก คุณเกือบจะแน่นอนไม่ต้องการใช้ที่เป็นอันตรายแทน "ผู้ใช้" หรือ "จอง" 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% แต่อนุญาตให้มีการควบคุมชื่อคอลัมน์เมื่อพวกเขา "เข้ามา" เป็นสตริงแบบไดนามิกและอาจมีการเปลี่ยนแปลงในผู้ใช้ปลายทาง นอกจากนี้ไม่จำเป็นต้องสร้างอาเรย์ด้วยชื่อคอลัมน์และประเภทตารางของคุณเนื่องจากมันจะถูกสกัดจาก data_schema

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.