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의 매개 변수로 대체 될 수 없습니다.

이 경우 단순히 데이터를 수동으로 필터링하고 삭제해야합니다. 이를 수행하는 한 가지 방법은 쿼리를 동적으로 실행하는 함수에 속기 매개 변수를 전달한 다음 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"; }. 아이디어 주셔서 감사합니다.
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와 같은 추상화 라이브러리가 준비된 명령문의 두 가지 주요 목적을 무효화하기 때문에 해결할 수 있거나 해결할 수있는 것이 아닙니다 .1) 데이터베이스가 쿼리 실행 방법을 미리 결정하고 동일한 것을 사용하도록합니다. 여러 번 계획; 2) 쿼리의 논리를 변수 입력에서 분리하여 보안 문제를 방지합니다.


1
사실이지만 PDO의 준비 명령문 에뮬레이션을 설명하지는 않습니다 ( 아마도 SQL 객체 식별자를 매개 변수화 수는 있지만 아마 그렇게해서는 안된다는 데 동의합니다).
eggyal

1
@eggyal 에뮬레이션은 완전히 새로운 기능을 추가하는 것이 아니라 모든 DBMS 풍미에 표준 기능을 작동시키는 것을 목표로합니다. 식별자 자리 표시 자에도 DBMS에서 직접 지원하지 않는 고유 한 구문이 필요합니다. PDO는 상당히 낮은 수준의 래퍼이며 예를 들어 TOP/ LIMIT/ OFFSET절에 대한 SQL 생성 및 제공을 제공하지 않으므로 기능으로 약간 벗어났습니다.
IMSoP

13

나는 이것이 오래된 게시물 인 것을 보았지만 유용하다고 생각하고 @kzqai가 제안한 것과 비슷한 해결책을 공유 할 것이라고 생각했다.

나는 두 개의 매개 변수를받는 기능이 있습니다 ...

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/questions/4700623/pdos-query-vs-execute
Don

귀하의 예에는 반복되는 전화가 없습니다
귀하의 상식

4

전자를 사용하는 것이 전자를 사용하는 것보다 본질적으로 안전하지는 않으므로 입력이 매개 변수 배열이든 간단한 변수이든 상관없이 입력을 삭제해야합니다. 내가 함께 후자의 양식을 사용하여 아무것도 잘못 표시되지 않습니다 그래서 $table, 당신의 내용이 있는지 확인 제공 $table안전 (영숫자 플러스 밑줄?)를 사용하기 전에.


첫 번째 옵션이 작동하지 않는다는 것을 고려할 때 어떤 형태의 동적 쿼리 작성을 사용해야합니다.
Noah Goodrich

예, 문제는 작동하지 않는다고 언급했습니다. 나는 그런 식으로 그것을 시도하는 것이 몹시 중요하지 않은 이유를 설명하려고했습니다.
Adam Bellaire

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

@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 % 보안을 제공하지는 않지만 열이 동적 문자열로 "들어올 때"열 이름을 일부 제어 할 수 있으며 사용자 측에서 변경 될 수 있습니다. 또한 information_schema에서 추출되므로 테이블 열 이름과 유형으로 배열을 만들 필요가 없습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.