Add simple escape method for special characters to template query

The default mustache engine was using HTML escaping which breaks queries
if used with JSON etc. This commit adds escaping for:

```
\b  Backspace (ascii code 08)
\f  Form feed (ascii code 0C)
\n  New line
\r  Carriage return
\t  Tab
\v  Vertical tab
\"  Double quote
\\  Backslash
```

Closes #5473
This commit is contained in:
Simon Willnauer 2014-03-20 17:16:04 +01:00
parent b11d4a5871
commit 9cd3e850af
4 changed files with 146 additions and 13 deletions

View File

@ -12,7 +12,7 @@
index: test
type: testtype
id: 2
body: { "text": "value2" }
body: { "text": "value2 value3" }
- do:
indices.refresh: {}
@ -39,3 +39,10 @@
body: { "query": { "template": { "query": "{\"match_{{template}}\": {}}", "params" : { "template" : "all" } } } }
- match: { hits.total: 2 }
- do:
search:
body: { "query": { "template": { "query": "{\"query_string\": { \"query\" : \"{{query}}\" }}", "params" : { "query" : "text:\"value2 value3\"" } } } }
- match: { hits.total: 1 }

View File

@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.script.mustache;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.MustacheException;
import java.io.IOException;
import java.io.Writer;
/**
* A MustacheFactory that does simple JSON escaping.
*/
public final class JsonEscapingMustacheFactory extends DefaultMustacheFactory {
@Override
public void encode(String value, Writer writer) {
try {
escape(value, writer);
} catch (IOException e) {
throw new MustacheException("Failed to encode value: " + value);
}
}
public static Writer escape(String value, Writer writer) throws IOException {
for (int i = 0; i < value.length(); i++) {
final char character = value.charAt(i);
if (isEscapeChar(character)) {
writer.write('\\');
}
writer.write(character);
}
return writer;
}
public static boolean isEscapeChar(char c) {
switch(c) {
case '\b':
case '\f':
case '\n':
case '\r':
case '"':
case '\\':
case '\u000B': // vertical tab
case '\t':
return true;
}
return false;
}
}

View File

@ -18,7 +18,6 @@
*/
package org.elasticsearch.script.mustache;
import com.github.mustachejava.DefaultMustacheFactory;
import com.github.mustachejava.Mustache;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
@ -79,7 +78,7 @@ public class MustacheScriptEngineService extends AbstractComponent implements Sc
* */
public Object compile(String template) {
/** Factory to generate Mustache objects from. */
return (new DefaultMustacheFactory()).compile(new FastStringReader(template), "query-template");
return (new JsonEscapingMustacheFactory()).compile(new FastStringReader(template), "query-template");
}
/**

View File

@ -18,25 +18,27 @@
*/
package org.elasticsearch.script.mustache;
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.equalTo;
/**
* Mustache based templating test
* */
*/
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
private MustacheScriptEngineService qe;
private static String TEMPLATE = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
@Before
public void setup() {
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
@ -44,12 +46,70 @@ public class MustacheScriptEngineTest extends ElasticsearchTestCase {
@Test
public void testSimpleParameterReplace() {
{
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("boost_val", "0.3");
BytesReference o = (BytesReference) qe.execute(qe.compile(TEMPLATE), vars);
BytesReference o = (BytesReference) qe.execute(qe.compile(template), vars);
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
new String(o.toBytes(), Charset.forName("UTF-8")));
}
{
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"{{body_val}}\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> vars = new HashMap<String, Object>();
vars.put("boost_val", "0.3");
vars.put("body_val", "\"quick brown\"");
BytesReference o = (BytesReference) qe.execute(qe.compile(template), vars);
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"\\\"quick brown\\\"\"}}}, \"negative_boost\": 0.3 } }}",
new String(o.toBytes(), Charset.forName("UTF-8")));
}
}
@Test
public void testEscapeJson() throws IOException {
{
StringWriter writer = new StringWriter();
JsonEscapingMustacheFactory.escape("hello \n world", writer);
assertThat(writer.toString(), equalTo("hello \\\n world"));
}
{
StringWriter writer = new StringWriter();
JsonEscapingMustacheFactory.escape("\n", writer);
assertThat(writer.toString(), equalTo("\\\n"));
}
Character[] specialChars = new Character[]{'\f', '\n', '\r', '"', '\\', (char) 11, '\t', '\b' };
int iters = scaledRandomIntBetween(100, 1000);
for (int i = 0; i < iters; i++) {
int rounds = scaledRandomIntBetween(1, 20);
StringWriter escaped = new StringWriter();
StringWriter writer = new StringWriter();
for (int j = 0; j < rounds; j++) {
String s = getChars();
writer.write(s);
escaped.write(s);
char c = RandomPicks.randomFrom(getRandom(), specialChars);
writer.append(c);
escaped.append('\\');
escaped.append(c);
}
StringWriter target = new StringWriter();
assertThat(escaped.toString(), equalTo(JsonEscapingMustacheFactory.escape(writer.toString(), target).toString()));
}
}
private String getChars() {
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
for (int i = 0; i < string.length(); i++) {
if (JsonEscapingMustacheFactory.isEscapeChar(string.charAt(i))) {
return string.substring(0, i);
}
}
return string;
}
}