Báo cáo PDO PHP có thể chấp nhận tên bảng hoặc cột làm tham số không?


243

Tại sao tôi không thể chuyển tên bảng cho câu lệnh PDO đã chuẩn bị?

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

Có cách nào khác an toàn để chèn tên bảng vào truy vấn SQL không? Với sự an toàn, ý tôi là tôi không muốn làm

$sql = "SELECT * FROM $table WHERE 1"

Câu trả lời:


212

Tên bảng và cột CANNOT được thay thế bằng các tham số trong PDO.

Trong trường hợp đó, bạn chỉ muốn lọc và vệ sinh dữ liệu theo cách thủ công. Một cách để làm điều này là chuyển các tham số tốc ký cho hàm sẽ thực hiện truy vấn một cách linh hoạt và sau đó sử dụng một switch()câu lệnh để tạo một danh sách trắng các giá trị hợp lệ được sử dụng cho tên bảng hoặc tên cột. Bằng cách đó, không có đầu vào của người dùng nào đi thẳng vào truy vấn. Ví dụ:

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

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

Bằng cách không để trường hợp mặc định hoặc sử dụng trường hợp mặc định trả về thông báo lỗi, bạn đảm bảo rằng chỉ những giá trị bạn muốn sử dụng mới được sử dụng.


17
+1 cho các tùy chọn danh sách trắng thay vì sử dụng bất kỳ loại phương thức động nào. Một cách khác có thể là ánh xạ tên bảng có thể chấp nhận thành một mảng với các khóa tương ứng với đầu vào của người dùng tiềm năng (ví dụ: array('u'=>'users', 't'=>'table', 'n'=>'nonsensitive_data')v.v.)
Kzqai

4
Đọc qua điều này, tôi nhận thấy rằng ví dụ ở đây tạo ra SQL không hợp lệ cho đầu vào xấu, bởi vì nó không có default. Nếu sử dụng mẫu này, bạn nên gắn nhãn một trong số caseđó là defaulthoặc thêm trường hợp lỗi rõ ràng, chẳng hạn nhưdefault: throw new InvalidArgumentException;
IMSoP

3
Tôi đã suy nghĩ đơn giản if ( in_array( $tbl, ['users','products',...] ) { $sql = "SELECT * FROM $tbl"; }. Cảm ơn ý kiến.
Phil Tune

2
Tôi bỏ lỡ mysql_real_escape_string(). Có lẽ ở đây tôi có thể nói mà không cần ai đó nhảy vào và nói "Nhưng bạn không cần nó với PDO"
Rolf

Vấn đề khác là tên bảng động phá vỡ kiểm tra SQL.
Acyra

143

Để hiểu lý do tại sao ràng buộc tên bảng (hoặc cột) không hoạt động, bạn phải hiểu cách giữ chỗ trong các câu lệnh đã chuẩn bị: chúng không chỉ được thay thế bằng chuỗi (thoát phù hợp) và SQL được thực thi. Thay vào đó, một DBMS được yêu cầu "chuẩn bị" một câu lệnh đưa ra một kế hoạch truy vấn hoàn chỉnh về cách nó sẽ thực hiện truy vấn đó, bao gồm các bảng và chỉ mục nào nó sẽ sử dụng, sẽ giống nhau bất kể bạn điền vào chỗ dành sẵn như thế nào.

Kế hoạch cho SELECT name FROM my_table WHERE id = :valuesẽ giống như bất cứ điều gì bạn thay thế :value, nhưng dường như SELECT name FROM :table WHERE id = :valuekhông thể lập kế hoạch tương tự , vì DBMS không biết bạn thực sự sẽ chọn bảng nào.

Đây không phải là thứ mà một thư viện trừu tượng như PDO có thể hoặc nên hoạt động, vì nó sẽ đánh bại 2 mục đích chính của các câu lệnh được chuẩn bị: 1) để cho phép cơ sở dữ liệu quyết định trước cách chạy truy vấn và sử dụng tương tự lên kế hoạch nhiều lần; và 2) để ngăn chặn các vấn đề bảo mật bằng cách tách logic của truy vấn khỏi đầu vào biến.


1
Đúng, nhưng không giải thích cho việc mô phỏng câu lệnh chuẩn bị của PDO ( có thể hiểu được các tham số nhận dạng đối tượng SQL, mặc dù tôi vẫn đồng ý rằng có lẽ không nên).
eggyal 27/12/13

1
@eggyal Tôi đoán việc mô phỏng nhằm mục đích làm cho chức năng tiêu chuẩn hoạt động trên tất cả các hương vị DBMS, thay vì thêm chức năng hoàn toàn mới. Một trình giữ chỗ cho các định danh cũng sẽ cần một cú pháp riêng biệt không được hỗ trợ trực tiếp bởi bất kỳ DBMS nào. PDO là một trình bao bọc cấp độ thấp và không cung cấp ví dụ và tạo SQL cho TOP/ LIMIT/ OFFSETmệnh đề, vì vậy đây sẽ là một tính năng không phù hợp như một tính năng.
IMSoP

13

Tôi thấy đây là một bài viết cũ, nhưng tôi thấy nó hữu ích và nghĩ rằng tôi sẽ chia sẻ một giải pháp tương tự như những gì @kzqai đề xuất:

Tôi có một chức năng nhận hai tham số như ...

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

Bên trong tôi kiểm tra các mảng tôi đã thiết lập để đảm bảo chỉ có các bảng và cột có bảng "may mắn" có thể truy cập được:

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

Sau đó, kiểm tra PHP trước khi chạy PDO trông giống như ...

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
tốt cho giải pháp ngắn, nhưng tại sao không chỉ$pdo->query($sql)
jscripter

Chủ yếu là theo thói quen khi chuẩn bị các truy vấn phải ràng buộc một biến. Cũng đọc các cuộc gọi lặp lại nhanh hơn w / thực hiện tại đây stackoverflow.com/questions/4700623/pdos-query-vs-execute
Don

không có cuộc gọi lặp lại trong ví dụ
của bạn

4

Sử dụng cái trước đây vốn không an toàn hơn cái trước, bạn cần vệ sinh đầu vào cho dù đó là một phần của mảng tham số hay biến đơn giản. Vì vậy, tôi không thấy có gì sai khi sử dụng biểu mẫu sau $table, với điều kiện bạn phải chắc chắn rằng nội dung của $tablenó là an toàn (alphanum cộng với dấu gạch dưới?) Trước khi sử dụng nó.


Xem xét rằng tùy chọn đầu tiên sẽ không hoạt động, bạn phải sử dụng một số hình thức xây dựng truy vấn động.
Noah Goodrich

Vâng, câu hỏi đề cập đến nó sẽ không hoạt động. Tôi đã cố gắng mô tả lý do tại sao nó cực kỳ quan trọng để thậm chí cố gắng làm theo cách đó.
Adam Bellaire

3

(Trả lời muộn, tham khảo ý kiến ​​phụ của tôi).

Quy tắc tương tự áp dụng khi cố gắng tạo một "cơ sở dữ liệu".

Bạn không thể sử dụng một câu lệnh được chuẩn bị để ràng buộc một cơ sở dữ liệu.

I E:

CREATE DATABASE IF NOT EXISTS :database

sẽ không làm việc. Sử dụng một safelist thay thế.

Lưu ý bên lề: Tôi đã thêm câu trả lời này (dưới dạng wiki cộng đồng) vì nó thường được sử dụng để đóng câu hỏi, trong đó một số người đã đăng câu hỏi tương tự như vậy trong việc cố gắng liên kết cơ sở dữ liệu chứ không phải bảng và / hoặc cột.


0

Một phần của tôi tự hỏi nếu bạn có thể cung cấp chức năng vệ sinh tùy chỉnh của riêng bạn đơn giản như sau:

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

Tôi đã không thực sự nghĩ về nó, nhưng có vẻ như loại bỏ bất cứ thứ gì ngoại trừ các ký tự và dấu gạch dưới có thể hoạt động.


1
Tên bảng MySQL có thể chứa các ký tự khác. Xem dev.mysql.com/doc/refman/5.0/en/identifier.html
Phil

@PhilLaNasa thực sự là một số bảo vệ họ nên (cần tham khảo). Vì hầu hết DBMS là trường hợp lưu trữ tên không phân biệt trong các ký tự không phân biệt, ví dụ: MyLongTableNamethật dễ đọc, nhưng nếu bạn kiểm tra tên được lưu trữ thì có lẽ nó sẽ MYLONGTABLENAMEkhông dễ đọc, nên MY_LONG_TABLE_NAMEthực sự dễ đọc hơn.
mloureiro

Có một lý do rất chính đáng để không có chức năng này: bạn rất hiếm khi chọn tên bảng dựa trên đầu vào tùy ý. Bạn gần như chắc chắn không muốn người dùng độc hại thay thế "người dùng" hoặc "đặt chỗ" vào Select * From $table. Một danh sách trắng hoặc khớp mẫu nghiêm ngặt (ví dụ: "tên bắt đầu báo cáo_ chỉ theo sau 1 đến 3 chữ số") thực sự rất cần thiết ở đây.
IMSoP

0

Đối với câu hỏi chính trong chủ đề này, các bài đăng khác đã nói rõ lý do tại sao chúng ta không thể liên kết các giá trị với tên cột khi chuẩn bị các câu lệnh, vì vậy đây là một giải pháp:

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

Trên đây chỉ là một ví dụ, vì vậy không cần phải nói, sao chép-> dán sẽ không hoạt động. Điều chỉnh cho nhu cầu của bạn. Bây giờ điều này có thể không cung cấp bảo mật 100%, nhưng nó cho phép một số quyền kiểm soát tên cột khi chúng "đi vào" dưới dạng chuỗi động và có thể được thay đổi khi người dùng kết thúc. Hơn nữa, không cần phải xây dựng một số mảng với các tên và kiểu cột trong bảng của bạn vì chúng được trích xuất từ ​​information_schema.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.