如何将多值WHERE子句拆分为多个原语,以便与PHP中PDO准备的语句一起使用

如何将多值WHERE子句拆分为多个原语,以便与PHP中PDO准备的语句一起使用,php,pdo,Php,Pdo,我正在尝试为PDO准备的语句编写一个查询生成器 我有一个WHERE语句作为字符串,比如 "title = 'home' and description = 'this is just an example'" "id = 1 or title = 'home'" "title = home" etc... WHERE语句可能包含用户提供的数据,并且需要进行消毒,据我所知,使用准备好的语句是一种广泛使用的方法 我需要拆分where字符串以创建一个新字符串,如 $where = "title =

我正在尝试为PDO准备的语句编写一个查询生成器

我有一个WHERE语句作为字符串,比如

"title = 'home' and description = 'this is just an example'"
"id = 1 or title = 'home'"
"title = home"
etc...
WHERE语句可能包含用户提供的数据,并且需要进行消毒,据我所知,使用准备好的语句是一种广泛使用的方法

我需要拆分where字符串以创建一个新字符串,如

$where = "title = :title AND description = :description";
和一个数组一样

$params = array(':title' => 'home', :description = 'this is just an example');
对我来说,困难在于我不知道在原始字符串中会传递多少个不同的过滤器

对于如何实现这一目标的任何帮助,我们都将不胜感激

下面是我的函数,它接受上述两个分割原语

function select($table, $fields = array(), $where = "", $params = array(), $limit = '', $fetchStyle = PDO::FETCH_ASSOC) {
    global $dbc, $dbq;

    if (empty($fields)) {
        $fields = "*";
    } else {
        $fields = implode(', ', $fields);
    }

    if (empty($where)) {
        $where = "1";
    }

    if ($limit != '' && is_int($limit)) {
        $limit_include = "LIMIT $limit";
    }

    //create query
    $query = "SELECT $fields FROM $table WHERE $where $limit_include";

    //prepare statement
    $dbq = $dbc->query($query);
    $dbq->execute($params);

    return $dbq->fetchAll($fetchStyle);
}

好的,我为你写了一个解析器。但首先要做几件事

这并不像最初看起来的那么琐碎。每当您允许用户直接在sql中输入“东西”时,您必须非常小心。因此,我使用的这种方法为数据提供了某种程度的卫生条件。这是因为所有的“位”必须匹配正则表达式才能通过。这些都不提供引号、反斜杠或其他对sql注入有用的东西。唯一的例外是封装字符串(单引号内的字符串)的正则表达式

但是,我必须强调,这并不能保证不可能通过它传递SQL注入代码。我之所以这么说,是因为我在这方面花的时间很少,测试的时间也很少。需要记住的是,查询字符串的任何部分都容易受到sql注入的影响,而不仅仅是值。如果您允许用户传入以下内容:

   "title = 'home' and description = 'this is just an example'"
他们可以通过这个:

   ";DROP DATABASE"
现在有了防止运行多个查询的保护措施,但我的观点是简单地进行字符串替换或简单的Regx是不够的。我还添加了一个“禁止”字列表。使用这些词时必须用单引号括起来。它们是MySQL中的常见操作,不应该出现在WHERE子句中。例如:

  • 下降
  • 删除
  • 展示
  • 改变
等等。。。现在,由于它们没有在函数
parse
的switch语句中定义,它们将由
default
案例拾取,该案例引发异常

也有很多变化,我试着涵盖最常见的事情。这些都没有出现在你们的例子中。我的意思是这样的:

  • “title='home'或title='user'”
    对同一列的多次使用(具有不同的值)
  • “标题在('home','user','foo',1,3)”
    IN
  • “标题不为空”
    为空
  • 在其他操作中,您只有
    =
    我包含了这个regx
    '=\124; \\ 124; \>=\ 124; \值集

    这就是我的想法(基于词汇分析):

    你可以测试一下


    希望它对你有用

    就我个人而言,我会为它做一个lexer/parser类的设置。你可以在我的github上看到这个方法的一个例子,它是用来解析Jason对象的(Jason缺少引号)。同样的方法可以用来解析任何字符串,你只需要构建标记并解析它们。我可以为您设置一个,但这需要一分钟,即使这样,您也可能需要对函数
    WHERE DATE(create_DATE)=
    等进行更改。好的,这是一个空版本。我不介意为你做一个,但现在是新年,所以我可能明年就完成不了了。。。。感谢@ArtisticPhoenix,我将检查此结果。我需要传递
    select
    函数a-string,其中包含用于SQL模板的构造WHERE子句,例如
    'title=:title AND description=:description“
    值“:title”和“:description”是使用sql模板的占位符,需要构造,并且-一个assoc数组,其中键是生成的占位符,值是原始字符串中的值,例如
    数组(':title'=>'home',…)
    我希望这有意义?此示例字符串不正确
    “title=home”“
    或者主页是一个专栏。这是一个令人惊讶的回答。我将通读你的代码,一旦我理解了,我会把它放进去,让它工作。这绝对不是一个简单的问题,事实上对我来说这是一个相当复杂的问题。我想我可以从这段代码中学到很多东西,我真的很感谢你花时间和精力写了一个详细的解释,但也花了额外的心思来更广泛地思考我的问题。另外,仅供参考,因为这生成的原语用于准备好的语句中,SQL是预编译的,值不应影响SQL(即,
    “删除数据库”
    这不应该是个问题,因为值是在SQL编译后插入的,但是你考虑到了这些事情,这太棒了。我正在做的项目是一个小型的个人项目,只是为了学习,但是你的回答无疑会教给我比我在原始问题中预期的更多的东西,再次感谢你!看到了吗e错了,这不是一个值,因为您允许它们放置列名和比较运算符,换句话说,是整个Where子句,而不仅仅是“原语”正如您所说。如果您允许用户提供,查询的任何部分都可能受到注入的影响。感谢您的评论,我做了一些进一步的阅读,似乎PDO准备的语句可以帮助避免一些简单的注入攻击,但对于复杂或二级攻击,似乎还需要采取进一步的步骤。我发现这篇文章真的很有用有趣。@Jamie你发现答案上写的完全是废话。
    //For debugging
    error_reporting(-1);
    ini_set('display_errors', 1);
    echo "<pre>";
    
    function parse($subject, $tokens)
    {
        $types = array_keys($tokens);
        $patterns = [];
        $lexer_stream = [];
        $result = false;
        foreach ($tokens as $k=>$v){
            $patterns[] = "(?P<$k>$v)";
        }
        $pattern = "/".implode('|', $patterns)."/i";
        if (preg_match_all($pattern, $subject, $matches, PREG_OFFSET_CAPTURE)) {
            //print_r($matches);
            foreach ($matches[0] as $key => $value) {
                $match = [];
                foreach ($types as $type) {
                    $match = $matches[$type][$key];
                    if (is_array($match) && $match[1] != -1) {
                        break;
                    }
                }
                $tok  = [
                    'content' => $match[0],
                    'type' => $type,
                    'offset' => $match[1]
                ];
                $lexer_stream[] = $tok;
            }
            $result = parseTokens( $lexer_stream );
        }
        return $result;
    }
    function parseTokens( array &$lexer_stream ){
    
        $column = '';
        $params = [];
        $sql = '';
    
        while($current = current($lexer_stream)){
            $content = $current['content'];
            $type = $current['type'];
            switch($type){
                case 'T_WHITESPACE':
                case 'T_COMPARISON':
                case 'T_PAREN_OPEN':
                case 'T_PAREN_CLOSE':
                case 'T_COMMA':
                case 'T_SYMBOL':
                    $sql .= $content;
                    next($lexer_stream);
                break;
                case 'T_COLUMN':
                    $column = $content;
                    $sql .= $content;
                    next($lexer_stream);
                break;
                case 'T_OPPERATOR':
                case 'T_NULL':
                    $column = '';
                    $sql .= $content;
                    next($lexer_stream);
                break;
                case 'T_ENCAP_STRING': 
                case 'T_NUMBER':
                    if(empty($column)){
                        throw new Exception('Parse error, value without a column name', 2001);
                    }
    
                    $value = trim($content,"'");
    
                    $palceholder = createPlaceholder($column, $value, $params);
    
                    $params[$palceholder] = $value;
                    $sql .= $palceholder;
                    next($lexer_stream);
                break;
                case 'T_IN':
                    $sql .= $content;
                    parseIN($column, $lexer_stream, $sql, $params);
                break;
                case 'T_EOF': return ['params' => $params, 'sql' => $sql];
    
                case 'T_UNKNOWN':
                case '':
                default:
                    $content = htmlentities($content);
                    print_r($current);
                    throw new Exception("Unknown token $type value $content", 2000);
            }
        }
    }
    
    function createPlaceholder($column, $value, $params){
        $placeholder = ":{$column}";
    
        $i = 1;
        while(isset($params[$placeholder])){
    
            if($params[$placeholder] == $value){
                break;
            }
    
            $placeholder = ":{$column}_{$i}";
            ++$i;
        }
    
        return $placeholder;
    }
    
    function parseIN($column, &$lexer_stream, &$sql, &$params){
        next($lexer_stream);
    
        while($current = current($lexer_stream)){
            $content = $current['content'];
            $type = $current['type'];
            switch($type){
                case 'T_WHITESPACE':
                case 'T_COMMA':
                    $sql .= $content;
                    next($lexer_stream);
                break; 
                case 'T_ENCAP_STRING':
                case 'T_NUMBER':
                    if(empty($column)){
                        throw new Exception('Parse error, value without a column name', 2001);
                    }
    
                    $value = trim($content,"'");
    
                    $palceholder = createPlaceholder($column, $value, $params);
    
                    $params[$palceholder] = $value;
                    $sql .= $palceholder;
                    next($lexer_stream);
                break;    
                case 'T_PAREN_CLOSE':
                    $sql .= $content;
                    next($lexer_stream);
                    return;
                break;          
                case 'T_EOL':
                    throw new Exception("Unclosed call to IN()", 2003);
    
                case 'T_UNKNOWN':
                default:
                    $content = htmlentities($content);
                    print_r($current);
                    throw new Exception("Unknown token $type value $content", 2000);
            }
        }
        throw new Exception("Unclosed call to IN()", 2003);
    }
    
    /**
     * token should be "name" => "regx"
     * 
     * Order is important
     * 
     * @var array $tokens
     */
    $tokens = [
        'T_WHITESPACE'      => '[\r\n\s\t]+',
        'T_ENCAP_STRING'    => '\'.*?(?<!\\\\)\'',
        'T_NUMBER'          => '\-?[0-9]+(?:\.[0-9]+)?',
        'T_BANNED'          => 'SELECT|INSERT|UPDATE|DROP|DELETE|ALTER|SHOW',
        'T_COMPARISON'      => '=|\<|\>|\>=|\<=|\<\>|!=|LIKE',
        'T_OPPERATOR'       => 'AND|OR',
        'T_NULL'            => 'IS NULL|IS NOT NULL',
        'T_IN'              => 'IN\s?\(',
        'T_COLUMN'          => '[a-z_]+',
        'T_COMMA'           => ',',
        'T_PAREN_OPEN'      => '\(',
        'T_PAREN_CLOSE'      => '\)',
        'T_SYMBOL'          => '[`]',
        'T_EOF'             => '\Z',
        'T_UNKNOWN'         => '.+?'
    ];
    
    $tests = [
        "title = 'home' and description = 'this is just an example'",
        "title = 'home' OR title = 'user'",
        "id = 1 or title = 'home'",
        "title IN('home','user', 'foo', 1, 3)",
        "title IS NOT NULL",
    ];
    
    /* the loop here is for testing only, obviously call it one time */
    foreach ($tests as $test){   
        print_r(parse($test,$tokens));
        echo "\n".str_pad(" $test ", 100, "=", STR_PAD_BOTH)."\n";  
    }
    
    Array
    (
        [params] => Array
            (
                [:title] => home
                [:description] => this is just an example
            )
    
        [sql] => title = :title and description = :description
    )
    
    ========== title = 'home' and description = 'this is just an example' ==========
    Array
    (
        [params] => Array
            (
                [:title] => home
                [:title_1] => user
            )
    
        [sql] => title = :title OR title = :title_1
    )
    
    ======================= title = 'home' OR title = 'user' =======================
    Array
    (
        [params] => Array
            (
                [:id] => 1
                [:title] => home
            )
    
        [sql] => id = :id or title = :title
    )
    
    =========================== id = 1 or title = 'home' ===========================
    Array
    (
        [params] => Array
            (
                [:title] => home
                [:title_1] => user
                [:title_2] => foo
                [:title_3] => 1
                [:title_4] => 3
            )
    
        [sql] => title IN(:title,:title_1, :title_2, :title_3, :title_4)
    )
    
    ===================== title IN('home','user', 'foo', 1, 3) =====================
    Array
    (
        [params] => Array
            (
            )
    
        [sql] => title IS NOT NULL
    )
    
    ============================== title IS NOT NULL ===============================