- 更新日: 2014年5月21日
- Elasticsearch
elasticsearch-ruby で外部入力から検索時の json 用文字列のエスケープ処理
Elasticsearch の Ruby クライアントである elasticsearch-ruby を使って、例えばウェブページの検索フォームでユーザーの外部入力から検索を行う場合、外部入力された文字列を適切にエスケープすることが必要なはずです。適当にぐぐって調べてみましたけど、NoSQL なデータベースに対するインジェクション攻撃のようなトピックは意外と情報が少ない。
以下これまでに作業を行った Elasticsearch 関連のトピックです。新しいのが上。
Elasticsearch を Ruby から使う | EasyRamble
ElasticsearchにMySQLからデータ挿入、JDBC River Pluginのインストールと使い方 | EasyRamble
Elasticsearchのクエリとフィルターで簡単な検索を試す例 | EasyRamble
ElasticsearchのインストールとCSVからのデータ挿入 | EasyRamble
elasticsearch-ruby で検索を行う際の文字列エスケープ
なかなか情報が少ない中、以下の Google グループの elasticsearch-ruby に関するトピックがめちゃ役に立ちました。
[RUBY] : elasticsearch-ruby : Special characters not escaped by the library – Google グループ
“generally avoid the query_string
query for user facing searches” だそうです。retire した tire の gem 作者の方がお返事されているので信頼性高い。ユーザーが検索するような場面では、query_string のクエリ使用は避けて、simple_query_string のほうが良いかもとのこと。
以下 elasticsearch のドキュメントによると、simple_query_string は例外を投げず、かつクエリの invalid な箇所を破棄すると説明があります。
Simple Query String Query
Query String Query
Ruby で JSON の文字列データのエスケープ
以上を踏まえて… Elasticsearch はクエリを json で組み立てるので、まずは json データのエスケープについて調べました。
NoSQLを使うなら知っておきたいセキュリティの話(2):「JSON文字列へのインジェクション」と「パラメータの追加」 (1/2) – @IT
JSONのエスケープ | yohgaki’s blog
JSON でのエスケープ処理 (JSONの値に”””, “\” を含める場合の処理)
json のエスケープについては、各言語で用意されている API を使うのが確実かと思います。Ruby の場合は、JSON#generate(object, state = nil) -> String か Object#to_json を利用できる。require “json” が必要です。
Encode a Ruby string to a JSON string – Stack Overflow
1 2 3 4 5 6 |
pry(main)> string = "\" \\ / \n \r \t" => "\" \\ / \n \r \t" > string.to_json => "\"\\\" \\\\ / \\n \\r \\t\"" |
エスケープだらけでわけわかめな感じですが、elasticsearch の検索に渡してみます。
1 2 3 4 5 6 7 8 9 10 |
pry(main)> client.search index: 'ldgourmet', body: { query: { match: { name: string.to_json } } }, size: 1 2014-05-20 12:45:27 +0900: GET http://localhost:9200/ldgourmet/_search?size=1 [status:200, request:0.151s, query:0.002s] 2014-05-20 12:45:27 +0900: > {"query":{"match":{"name":"\"\\\" \\\\ / \\n \\r \\t\""}}} 2014-05-20 12:45:27 +0900: < {"took":2,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":0,"max_score":null,"hits":[]}} => {"took"=>2, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, "hits"=>{"total"=>0, "max_score"=>nil, "hits"=>[]}} |
う〜ん、”name”:”\”\\\” \\\\ / \\n \\r \\t\”” と json 文字列データの中でさらに \” で囲われているのでこのままじゃいけません。string.to_json の先頭の \” と最後の \” を削除して渡す。
1 2 3 4 5 6 7 8 9 10 |
pry(main)> client.search index: 'ldgourmet', body: { query: { match: { name: string.to_json[1..-2] } } }, size: 1 2014-05-20 12:52:38 +0900: GET http://localhost:9200/ldgourmet/_search?size=1 [status:200, request:0.036s, query:0.001s] 2014-05-20 12:52:38 +0900: > {"query":{"match":{"name":"\\\" \\\\ / \\n \\r \\t"}}} 2014-05-20 12:52:38 +0900: < {"took":1,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":0,"max_score":null,"hits":[]}} => {"took"=>1, "timed_out"=>false, "_shards"=>{"total"=>5, "successful"=>5, "failed"=>0}, "hits"=>{"total"=>0, "max_score"=>nil, "hits"=>[]}} |
これで良いかな。
Elasticsearch の予約語のエスケープ
また、Elasticsearch に特有の予約語がいくつかありまして、それらもエスケープさせる必要があります。
+ – && || ! ( ) { } [ ] ^ ” ~ * ? : \ /
AND OR NOT
以上が elasticsearch の予約語となっているので、これらもクエリの json に渡す際にエスケープする。以下 Stack Overflow で見つけたメソッドが大変参考になる。
ruby on rails 3.2 – Symbols in query-string for elasticsearch – Stack Overflow
また、調査の過程で見つけた Java の package org.apache.lucene.queryparser.classic の escape メソッド実装も参考になります。Elasticsearch の バックエンドである Apache Lucene 用パッケージの API。public static String escape(String s) の箇所です。
package org.json.simple で json 用の escape メソッド実装も見つけました。以下の static void escape(String s, StringBuffer sb) の箇所。
JSONValue.java – json-simple – JSON.simple – A simple Java toolkit for JSON – Google Project Hosting
独自の sanitize メソッド
以上のメソッドを参考するとともに String#to_json を利用して、外部入力からの elasticsearch 検索用に、文字列を sanitize するメソッドを作成しました。require “json” が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
def sanitize_string_for_elasticsearch(str) raise "invalid argument." unless str.is_a? String # escape str for json str = str.to_json[1..-2] # escape special characters except for " and \ # http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#_reserved_characters escaped_characters = Regexp.escape('+-&|!(){}[]^~*?:/') str = str.gsub(/([#{escaped_characters}])/, '\\\\\1') # escape AND, OR and NOT used by lucene as logical operators ['AND', 'OR', 'NOT'].each do |word| escaped_word = word.split('').map {|char| "\\#{char}" }.join('') str = str.gsub(/\s*\b(#{word.upcase})\b\s*/, " #{escaped_word} ") end str end |
利用する Rails コントローラーに上記メソッドを private メソッドで定義して、以下のように使う。
1 |
sanitized_word = sanitize_string_for_elasticsearch(params[:search_word]) |
pry で動作テスト。以下の文字列を渡してみます。
” \ \b \f \n \r \t
+ – & | ! ( ) { } [ ] ^ ~ * ? : /
AND OR NOT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
pry(main)> s1 = "\" \\ \b \f \n \r \t" => "\" \\ \b \f \n \r \t" pry(main)> s2 = "+ - & | ! ( ) { } [ ] ^ ~ * ? : /" => "+ - & | ! ( ) { } [ ] ^ ~ * ? : /" pry(main)> s3 = "AND OR NOT" => "AND OR NOT" pry(main)> sanitize_string_for_elasticsearch s1 => "\\\" \\\\ \\b \\f \\n \\r \\t" [112] pry(main)> sanitize_string_for_elasticsearch s2 => "\\+ \\- \\u0026 \\| \\! \\( \\) \\{ \\} \\[ \\] \\^ \\~ \\* \\? \\: \\/" [113] pry(main)> sanitize_string_for_elasticsearch s3 => " \\A\\N\\D \\O\\R \\N\\O\\T " |
良い感じにエスケープされています。
記号で検索する人はほぼ皆無でしょうから、さらに安全を期するために、検索フォームからの外部入力で記号の類の文字を一切受け付けないようにしても良いかもです。
以下、アルファベット・数字、ハイフン、ひらがな、カタカナ、漢字、半角スペースと全角スペース以外の記号などの入力文字を全部取り除いてしまうメソッド。ハイフン(-), AND, OR, NOT はエスケープさせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def strictly_sanitize_string_for_elasticsearch(str) # remove symbols str = str.gsub(/[^\w\-\p{Hiragana}\p{Katakana}ー-一-龠々 ]/, '') # escape AND, OR and NOT used by lucene as logical operators ['AND', 'OR', 'NOT'].each do |word| escaped_word = word.split('').map {|char| "\\#{char}" }.join('') str = str.gsub(/\s*\b(#{word.upcase})\b\s*/, " #{escaped_word} ") end # escape hyphen str = str.gsub(/(-)/, '\\\\\1') str end |
以下 pry で試行。
1 2 3 4 5 6 7 8 9 10 11 12 |
pry(main)> strictly_sanitize_string_for_elasticsearch s1 => " " pry(main)> strictly_sanitize_string_for_elasticsearch s2 => " \\- " pry(main)> strictly_sanitize_string_for_elasticsearch s3 => " \\A\\N\\D \\O\\R \\N\\O\\T " pry(main)> str = "--- Hello world AND 今日は! ---" => "--- Hello world AND 今日は! ---" pry(main)> strictly_sanitize_string_for_elasticsearch str => "\\-\\-\\- Hello world \\A\\N\\D 今日は \\-\\-\\-" |
こんな感じで記号は削除されてしまいますが、検索用途の目的であれば大して支障もないかと思います。ウェブページのフォームなどユーザーの外部入力から elasticsearch-ruby を使って検索を行う場合は、用途に応じていずれかのメソッドを使おうと思います。間違いや漏れなどありましたら、ぜひご指摘お願いいたします!
- – 参考リンク –
- ElasticSearch Users – how to properly escape special characters?
- Apache Lucene – Query Parser Syntax
- Insecure default in Elasticsearch enables remote code execution
- Do I have to worry about an MVEL attack? – Google グループ
- Module: JSON (Ruby 2.0.0)
- Class: JSON::Ext::Generator::State (Ruby 1.9.3)
- Elasticsearch の関連記事
- CentOS6にElasticsearchをインストールしMySQLからデータをインポート
- Rails で jQuery を使って Elasticsearch 全文検索による検索文字をハイライトさせる
- elasticsearch-ruby でトークナイザーを指定してトークン分割
- Elasticsearch を Ruby から使う
- ElasticsearchにMySQLからデータ挿入、JDBC River Pluginのインストールと使い方
- Elasticsearchのクエリとフィルターで簡単な検索を試す例
- ElasticsearchのインストールとCSVからのデータ挿入
Leave Your Message!