【问题标题】:Escape elasticsearch special characters in PHP在 PHP 中转义 elasticsearch 特殊字符
【发布时间】:2016-02-24 00:51:21
【问题描述】:

我想创建一个函数,通过在 PHP 中的字符前添加 \ 来转义 elasticsearch 特殊字符。 Elasticsearch 使用的特殊字符是: + - = && || >

我对正则表达式不是很熟悉,但我发现了一段代码可以简单地删除特殊字符,但我更喜欢转义它们,因为这可能是相关的。我使用的代码:

$s_input = 'The next chars should be escaped: + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ / Did it work?';
$search_query = preg_replace('/(\+|\-|\=|\&|\||\!|\(|\)|\{|\}|\[|\]|\^|\"|\~|\*|\<|\>|\?|\:|\\\\)/', '', $s_input);

这个输出:

The next chars should be escaped / Did it work

所以有两个问题:这段代码删除了特殊字符,而我想用\ 转义它们。此外:此代码不会转义 \。有谁知道如何逃避 Elasticsearch 特殊字符?

【问题讨论】:

  • 我建议您阅读preg_replace() 的用途。我在上面的代码中只看到一个空字符串作为替换参数。
  • 是的,但是当我在替换字符串中放置 \ 或双 \\ 时,它仍然不起作用。抱歉,我应该在我的问题中提到这一点。

标签: php regex elasticsearch


【解决方案1】:

您可以将preg_matchbackreferences 一起使用,因为 stribizhev 已经注意到它(最简单的方式):

$string = "The next chars should be escaped: + - = && || > < ! ( ) { } [ ] ^ \" ~ * ? : \ / Did it work?"; 

function escapeElasticReservedChars($string) {
    $regex = "/[\\+\\-\\=\\&\\|\\!\\(\\)\\{\\}\\[\\]\\^\\\"\\~\\*\\<\\>\\?\\:\\\\\\/]/";
    return preg_replace($regex, addslashes('\\$0'), $string);
}
echo escapeElasticReservedChars($string);

或使用preg_match_callback 函数来实现。感谢回调,您将能够拥有当前匹配并对其进行编辑。

将被调用并传递匹配元素数组的回调 在主题字符串中。回调应该返回替换 细绳。这是回调签名:

这里正在行动:

<?php 
$string = "The next chars should be escaped: + - = && || > < ! ( ) { } [ ] ^ \" ~ * ? : \ / Did it work?"; 

function escapeElasticSearchReservedChars($string) {
    $regex = "/[\\+\\-\\=\\&\\|\\!\\(\\)\\{\\}\\[\\]\\^\\\"\\~\\*\\<\\>\\?\\:\\\\\\/]/";
    $string = preg_replace_callback ($regex, 
        function ($matches) { 
            return "\\" . $matches[0]; 
        }, $string); 
    return $string;
}
echo escapeElasticSearchReservedChars($string);

输出:The next chars should be escaped\: \+ \- \= \&amp;\&amp; \|\| \&gt; \&lt; \! \( \) \{ \} \[ \] \^ \" \~ \* \? \: \\ \/ Did it work\?

【讨论】:

  • 反向引用优于 preg_replace_callback。您使解决方案过于复杂。
  • 我同意你对简单性的看法,但是当你说“更好”时,你是在谈论性能吗?我更新了我的答案:) TY
  • 应该是 $regex = '/[\\+\-\\=\\&\\|\\!\(\)\\{\\}[]\\^\\ \"\\~\*\\\\?\\:\\\\\\/]/',我认为你犯了一个小错误,因为符号 [,],- 有双 \\,
  • 为什么正则表达式字符串中的所有反斜杠?例如,正则表达式可以不以"/[\\+\\-\\=\\&amp;\\|\\! 开头,而是以"/[-+=&amp;|! 开头,因为这些都不是字符类中的正则表达式元字符。该列表中的所有字符都可以按原样使用(但是,破折号必须是列表中的第一个或最后一个字符)。
【解决方案2】:

如果有人正在寻找稍微冗长(但可读!)的解决方案:

public function escapeElasticsearchValue($searchValue)
{
    $searchValue = str_replace('\\', '\\\\', $searchValue);
    $searchValue = str_replace('*', '\\*', $searchValue);
    $searchValue = str_replace('?', '\\?', $searchValue);
    $searchValue = str_replace('+', '\\+', $searchValue);
    $searchValue = str_replace('-', '\\-', $searchValue);
    $searchValue = str_replace('&&', '\\&&', $searchValue);
    $searchValue = str_replace('||', '\\||', $searchValue);
    $searchValue = str_replace('!', '\\!', $searchValue);
    $searchValue = str_replace('(', '\\(', $searchValue);
    $searchValue = str_replace(')', '\\)', $searchValue);
    $searchValue = str_replace('{', '\\{', $searchValue);
    $searchValue = str_replace('}', '\\}', $searchValue);
    $searchValue = str_replace('[', '\\[', $searchValue);
    $searchValue = str_replace(']', '\\]', $searchValue);
    $searchValue = str_replace('^', '\\^', $searchValue);
    $searchValue = str_replace('~', '\\~', $searchValue);
    $searchValue = str_replace(':', '\\:', $searchValue);
    $searchValue = str_replace('"', '\\"', $searchValue);
    $searchValue = str_replace('=', '\\=', $searchValue);
    $searchValue = str_replace('/', '\\/', $searchValue);

    // < and > can’t be escaped at all. The only way to prevent them from
    // attempting to create a range query is to remove them from the query
    // string entirely
    $searchValue = str_replace('<', '', $searchValue);
    $searchValue = str_replace('>', '', $searchValue);

    return $searchValue;
}

【讨论】:

  • str_replace() 接受第一个和第二个参数的数组。我不会使用或推荐这个答案。这使得,大约 20 次通过相同的输入字符串?只需使用正则表达式通过一次即可避免脚本膨胀。
  • @mickmackusa 如果preg_replace() 比使用 str_replace() 进行 20 次传递更便宜,您是否真的测量过? PCRE 函数并不便宜。
  • 如果您怀疑我的建议,欢迎您建立一个基准进行审查。我的观点是关于直接性。单个正则表达式模式可以一次完成匹配; str_replace() 不能,它将对每个替换进行全面扫描。在我看来,尝试基准测试的变量太多。
  • 我同意 PCRE 函数并不便宜,但在某些情况下,它们会淘汰大量函数调用的替代方案。如:stackoverflow.com/a/47595575/2943403
【解决方案3】:

完全披露,我从未使用过弹性搜索,我的建议不是来自个人经验,甚至不是用弹性搜索测试过的。我根据我对正则表达式和字符串操作技能的了解产生了这个建议。如果有人发现漏洞,我很乐意收到您的评论。

我的sn-p:

  • 首先删除字符串中所有出现的&lt;&gt; 然后
  • 检查单次出现的保留字符列表中的字符或紧跟相同字符的与号或管道 - 所有这些限定字符都使用反斜杠进行转义。

代码:(Demo)

$string = "To be escaped: + - = && || > < ! ( ) { } [ ] ^ \" ~ * ? : \ / triple ||| and split '&<&'"; 

echo escapeElasticSearchReservedChars($string);

function escapeElasticSearchReservedChars(string $string): string
{
    return preg_replace(
        [
            '_[<>]+_',
            '_[-+=!(){}[\]^"~*?:\\/\\\\]|&(?=&)|\|(?=\|)_',
        ],
        [
            '',
            '\\\\$0',
        ],
        $string
    );
}

输出:

To be escaped\: \+ \- \= \&& \||   \! \( \) \{ \} \[ \] \^ \" \~ \* \? \: \\ \/ triple \|\|| and split '\&&'

首先删除&lt;&gt; 的原因是,有人不能尝试破解替换的设计并尝试传入|&gt;|,否则会阻止两个连续管道的适当转义(在&gt; 已被删除)。

【讨论】:

  • 干得好!我做了一些额外的基准测试,似乎这个实现在 PHP 8 中具有最高的性能:3v4l.org/9F0Pk——旧版本可能会从删除 &lt;&gt; 中受益,因此 preg_replace() 不需要传递数组。 PHP 8 的 PCRE 初始化似乎比旧版本更快。
  • 另请注意,Elasticsearch (ES) 有两种不同的查询类型。这个答案是 ES“查询字符串”的正确编码,但是 ES“简单查询字符串”需要完全不同的编码!
  • 我必须相信你。谢谢你的澄清。
  • “简单查询字符串”语法:elastic.co/guide/en/elasticsearch/reference/current/… 与“查询字符串”语法:elastic.co/guide/en/elasticsearch/reference/5.5/…
【解决方案4】:

此版本应被视为已弃用,因为 https://stackoverflow.com/a/68393421/334451 具有更好的性能并产生正确的输出。

似乎所有给定的答案都没有真正遵循文档,所以这是另一个正确编码任何不受信任的输入的答案:

/**
 * @param string $s untrusted user input
 * @return string safe string to be used in `query_string` argument to elasticsearch
 */
function escapeForElasticSearch($s)
{
    static $keys = array();
    static $values = array();
    if (!$keys)
    {
        # https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-query-string-query.html#_reserved_characters
        $replacements = array(
            ">" => "", # cannot be safely encoded
            "<" => "", # cannot be safely encoded
            "\\" => "\\\\", # must be done first to not double encode later backslashes!
            "+" => "\\+",
            "-" => "\\-",
            "=" => "\\=",
            "&" => "\\&",
            "|" => "\\|",
            "!" => "\\!",
            "(" => "\\(",
            ")" => "\\)",
            "{" => "\\{",
            "}" => "\\}",
            "[" => "\\[",
            "]" => "\\]",
            "^" => "\\^",
            "\"" => "\\\"",
            "~" => "\\~",
            "*" => "\\*",
            "?" => "\\?",
            ":" => "\\:",
            "/" => "\\/",
        );
        $keys = array_keys($replacements);
        $values = array_values($replacements);
    }
    return str_replace($keys, $values, $s);
}

请注意,&amp;| 本身并不特殊,但要正确处理这些字符的奇数数量比仅对这些字符的每个实例进行编码更难。

请注意,首先删除所有必须删除的字符很重要。否则攻击者可以使用序列在字符串已经部分编码后删除中间的字符。

【讨论】:

  • 请注意,官方文档没有说明控制字符 (U+0000 ... U+001F) 或不合适的低或高代理是否会导致问题。如果您期望二进制输入,则删除它们可能是个好主意。
  • 我不认为array_values() 是有益的。为了让研究人员明白,这个脚本对输入字符串进行了 22 次单独的完整扫描。
  • 我将代码更新为不在循环中调用 array_keys() 或 array_values(),现在这与另一个问题上最好的不可读代码具有相同的性能。我将简单的基准测试脚本放在gist.github.com/mikkorantalainen/…,所以你可以看到 preg_match() 确实非常昂贵。在此实现中是否对字符串进行全面扫描并不重要。你同意这比正则表达式实现更容易维护吗?
  • 我不赞同接受的答案。我运行了您的基准测试并包含了我自己的脚本(其行为略有不同),并且性能差异非常相似,在我看来可以忽略不计。 3v4l.org/5KR7n(1) Looping 200000 calls took 1.07 s (2) Looping 200000 calls took 1.64 s (3) Looping 200000 calls took 1.12 s
【解决方案5】:

简单的方法是使用单个字符类进行匹配。
唯一的问题是使用什么作为分隔符(为了便于阅读)。

使用@作为正则分隔符,它的

查找:'@[-+=&amp;|&gt;&lt;!(){}[\]^"~*?:\\\/]@'
替换:'\\$0'


但是,如果实际字符已经被转义了怎么办?
然后怎样呢?

一个解决方案是找到那些没有转义的。

查找:'@(?&lt;!\\\)(?:\\\\\\\)*\K(?:[-+=&amp;|&gt;&lt;!(){}[\]^"~*?:/]|\\\(?!\\\))@'
替换:'\\$0'

格式化:

 (?<! \\ )                     # Not an escape behind 
 (?: \\ \\ )*                  # Possible even number of escapeds
 \K                            # Don't include the previous escapes in match
 (?:
      [-+=&|><!(){}[\]^"~*?:/]      # Either 1 of these special characters
   |                              # or,
      \\                            # An escape character that is
      (?! \\ )                      # not followed by escape itself.
 )

【讨论】:

    猜你喜欢
    • 2017-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-06
    • 2015-05-23
    • 2016-01-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多