如何将多值WHERE子句拆分为多个原语,以便与PHP中PDO准备的语句一起使用
我正在尝试为PDO准备的语句编写一个查询生成器 我有一个WHERE语句作为字符串,比如如何将多值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 =
"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'”
IN“标题在('home','user','foo',1,3)”
为空“标题不为空”
- 在其他操作中,您只有
我包含了这个regx=
'=\124; \\ 124; \>=\ 124; \值集 这就是我的想法(基于词汇分析): 你可以测试一下
希望它对你有用 就我个人而言,我会为它做一个lexer/parser类的设置。你可以在我的github上看到这个方法的一个例子,它是用来解析Jason对象的(Jason缺少引号)。同样的方法可以用来解析任何字符串,你只需要构建标记并解析它们。我可以为您设置一个,但这需要一分钟,即使这样,您也可能需要对函数
等进行更改。好的,这是一个空版本。我不介意为你做一个,但现在是新年,所以我可能明年就完成不了了。。。。感谢@ArtisticPhoenix,我将检查此结果。我需要传递WHERE DATE(create_DATE)=
函数a-string,其中包含用于SQL模板的构造WHERE子句,例如select
值“:title”和“:description”是使用sql模板的占位符,需要构造,并且-一个assoc数组,其中键是生成的占位符,值是原始字符串中的值,例如'title=:title AND description=:description“
我希望这有意义?此示例字符串不正确数组(':title'=>'home',…)
或者主页是一个专栏。这是一个令人惊讶的回答。我将通读你的代码,一旦我理解了,我会把它放进去,让它工作。这绝对不是一个简单的问题,事实上对我来说这是一个相当复杂的问题。我想我可以从这段代码中学到很多东西,我真的很感谢你花时间和精力写了一个详细的解释,但也花了额外的心思来更广泛地思考我的问题。另外,仅供参考,因为这生成的原语用于准备好的语句中,SQL是预编译的,值不应影响SQL(即,“title=home”“
这不应该是个问题,因为值是在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 ===============================