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:
parent
b11d4a5871
commit
9cd3e850af
|
@ -12,7 +12,7 @@
|
||||||
index: test
|
index: test
|
||||||
type: testtype
|
type: testtype
|
||||||
id: 2
|
id: 2
|
||||||
body: { "text": "value2" }
|
body: { "text": "value2 value3" }
|
||||||
- do:
|
- do:
|
||||||
indices.refresh: {}
|
indices.refresh: {}
|
||||||
|
|
||||||
|
@ -39,3 +39,10 @@
|
||||||
body: { "query": { "template": { "query": "{\"match_{{template}}\": {}}", "params" : { "template" : "all" } } } }
|
body: { "query": { "template": { "query": "{\"match_{{template}}\": {}}", "params" : { "template" : "all" } } } }
|
||||||
|
|
||||||
- match: { hits.total: 2 }
|
- match: { hits.total: 2 }
|
||||||
|
|
||||||
|
- do:
|
||||||
|
search:
|
||||||
|
body: { "query": { "template": { "query": "{\"query_string\": { \"query\" : \"{{query}}\" }}", "params" : { "query" : "text:\"value2 value3\"" } } } }
|
||||||
|
|
||||||
|
|
||||||
|
- match: { hits.total: 1 }
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,7 +18,6 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.script.mustache;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
import com.github.mustachejava.DefaultMustacheFactory;
|
|
||||||
import com.github.mustachejava.Mustache;
|
import com.github.mustachejava.Mustache;
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.Nullable;
|
||||||
import org.elasticsearch.common.component.AbstractComponent;
|
import org.elasticsearch.common.component.AbstractComponent;
|
||||||
|
@ -79,7 +78,7 @@ public class MustacheScriptEngineService extends AbstractComponent implements Sc
|
||||||
* */
|
* */
|
||||||
public Object compile(String template) {
|
public Object compile(String template) {
|
||||||
/** Factory to generate Mustache objects from. */
|
/** Factory to generate Mustache objects from. */
|
||||||
return (new DefaultMustacheFactory()).compile(new FastStringReader(template), "query-template");
|
return (new JsonEscapingMustacheFactory()).compile(new FastStringReader(template), "query-template");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -18,25 +18,27 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.script.mustache;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
|
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mustache based templating test
|
* Mustache based templating test
|
||||||
* */
|
*/
|
||||||
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
||||||
private MustacheScriptEngineService qe;
|
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
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
|
qe = new MustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
|
||||||
|
@ -44,12 +46,70 @@ public class MustacheScriptEngineTest extends ElasticsearchTestCase {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSimpleParameterReplace() {
|
public void testSimpleParameterReplace() {
|
||||||
Map<String, Object> vars = new HashMap<String, Object>();
|
{
|
||||||
vars.put("boost_val", "0.3");
|
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
||||||
BytesReference o = (BytesReference) qe.execute(qe.compile(TEMPLATE), vars);
|
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
|
||||||
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
Map<String, Object> vars = new HashMap<String, Object>();
|
||||||
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
|
vars.put("boost_val", "0.3");
|
||||||
new String(o.toBytes(), Charset.forName("UTF-8")));
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue