miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

Solr検索クエリの(たぶん)正しいエスケープ

Solr で検索するときの特殊文字エスケープ方法を調べていたんですが、ちゃんと実装&説明されているケースが見あたらなかったので、自分なりに説明を書いておきます。

想定するケース

ユーザが入力した文字列 str を使って、

query = "title:" + escape(str);

のように検索式を組み立てる場合に、その文字列自体で検索するにはどのようにエスケープすればいいかを考えます。デフォルトのクエリパーサの動作しか調べていませんので、別のクエリパーサ(DisMax 等?)を使っている場合は何か変更が必要かもしれません。

エスケープの方法

エスケープは「\」を直前に挿入する前提で説明します。ダブルクォートで囲むことでも特殊文字を解釈させなくすることは可能ですが、フレーズ検索になってしまって期待とは違う結果が返ってしまうこともあるでしょうし、フレーズ検索の内部でも結局「"」のエスケープは必要になりますので。

SolrQueryParserBase の実装 を見ると、エスケープが不要な文字を「\」でエスケープした場合、例えば「\あ」とした場合は「あ」そのものとして解釈されるようです。\uXXXX によるエスケープも解釈してくれるようですが、特にそうする必要もないので「\」を使ったエスケープを使えばいいでしょう。

エスケープの対象

以下の 3 つをエスケープする必要があります。

エスケープが不要な文字を「\」でエスケープしても問題ないので、極端な話をしてしまうと、全ての文字をエスケープしてしまっても問題ないといえばないです。

(1) 特殊文字

Solr Wiki に載っている情報です。

Certain characters are special (pre-4.0) or (4.0+) and those characters need to be escaped using quotes or a backslash if you want them to be treated as literals.

ということで、Solr 4.0 以降では

+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /

エスケープが必要です。Solr 4.0 から正規表現が使えるようになって「/」のエスケープも必要になっているんですが、最近書かれた記事でもこれが足りないものは結構ありました。

ちなみに、「&&」「||」の 2 つを「\&&」「\||」にエスケープしている実装を見かけて「この実装だと『&&&』が入力された場合に『\&&&』になる(『&&』が残る)から SyntaxError になるのでは?」とも思ったのですが、間に空白がないとトークンとして扱われないので問題なさそうでした。

(2) 空白文字

ユーザが「a b」を入力した場合、そのまま文字列を組み立てると「title:a b」となり、df(デフォルト検索フィールド)が「text」、q.op(デフォルト検索式)が「OR」の場合は「title:a OR text:b」の意味になります。たぶんこれは期待した結果ではないと思うので、空白もエスケープしましょう。

空白として扱われる文字は明言されていないのですが、 ソースコード を見ると

 | <#_WHITESPACE:  ( " " | "\t" | "\n" | "\r" | "\u3000") >

となっていました(U+3000 は全角空白です)ので、この 5 文字を含む形でエスケープしましょう。

SolrJ の ClientUtils.escapeQueryCharsCharacter.isWhitespace を使っており、この 5 文字以外の空白も全てエスケープしています。Unicode 対応の正規表現が使えるのであれば、これを見習って \s や [:space:] でマッチしたものをエスケープしてしまってもいいでしょう。(実装によってエスケープされる文字が違ったりするので、ちゃんとテストして使いましょう。)

(3) AND/OR/NOT

特殊文字は一切含まれていませんが、「title:AND」とすると

org.apache.solr.common.SolrException: org.apache.solr.search.SyntaxError:
Cannot parse 'title:AND': Encountered " <AND> "AND "" at line 1, column 6.

のように SyntaxError になってしまいますので、これもエスケープしておきましょう。

文字列が「AND」「OR」「NOT」のいずれかと一致する場合は、それぞれ「\AND」「\OR」「\NOT」にエスケープすればよいです。小文字を含む「and」「And」等はただの文字列として扱われますので、この 3 パターンだけ対応すれば大丈夫です。

一番対応が漏れやすいと思われる部分で、私も別システムでのエスケープ実装を見ていなかったらちゃんと実装できていなかったと思います。

実装例

調査が間に合わなかったので、実際に使っているコードは少々違うのですが・・・。Java で SolrJ を使っているのであれば、AND/OR/NOT に対応するために ClientUtils.escapeQueryChars を軽くラップすれば大丈夫です。

    private String escape(String str) {
        if ("AND".equals(str) || "OR".equals(str) || "NOT".equals(str)) {
            return "\\" + str;
        }
        return ClientUtils.escapeQueryChars(str);
    }

PHP だとたぶんこんな感じで期待通りに動作します。/u 修飾子を使わないと \s が U+3000 にマッチしないようなので要注意です。

<?php
function escape($str) {
    if (preg_match('/\A(?:AND|OR|NOT)\z/', $str)) {
        return '\\' . $str;
    }
    return preg_replace('#([+\-&|!(){}[\]^"~*?:\\\\/\s])#u', '\\\\\\1', $str);
}

セキュリティ的にはどうか

正しい(と思われる)エスケープ方法を説明してきましたが、クエリのエスケープに漏れたからといって、それがセキュリティ面で問題になってくることは少ないと思います。操作できるのは検索クエリなので、できるのはせいぜい全ドキュメントをヒットさせることくらいです。(上限値の設定が不適切なら、それはそれでメモリを枯渇させることも可能かもしれません。)

Solr 4.0 で正規表現が使えるようになっているので、効率の悪い正規表現を実行させて DoS に使えないかとも思ったのですが、DFAdk.brics.automaton が使われているようなので、その心配もありません。

ただしクライアントライブラリを使わずに自前で Solr サーバにリクエストを投げていて URL エンコードを怠っているような場合は、

aaa&fl=id:"<script>alert();</script>"

といった文字列を渡すことで、

/select?q=aaa&fl=id:"<script>alert();</script>"

のように本来 HTML エスケープが不要な項目に HTML を返させて XSS を起こすくらいはできるかもしれませんし、本来ユーザに見せるべきでないフィールドの値を返すこともできるかもしれません。自前で実装しているなら、クエリ中のエスケープなんかよりも先に URL のエンコードを気をつけたほうがよさそうです。

エスケープすれば十分か

  • 数値フィールドに数値以外を渡すと「Invalid Number」エラーになる
  • 入力値が長すぎると Jetty の「HttpParser Full」エラーになる

といった問題があるので、エスケープすればそれで OK というわけではないです。バリデーションもちゃんとやっておきましょう。