Skip to content

Commit c1115d2

Browse files
authored
Add a size limit to outputs from mustache (#114002) (#114297)
Backport #114002 to 8.16
1 parent e745c92 commit c1115d2

File tree

19 files changed

+181
-24
lines changed

19 files changed

+181
-24
lines changed

docs/changelog/114002.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 114002
2+
summary: Add a `mustache.max_output_size_bytes` setting to limit the length of results from mustache scripts
3+
area: Infra/Scripting
4+
type: enhancement
5+
issues: []

libs/logstash-bridge/src/main/java/org/elasticsearch/logstashbridge/script/ScriptServiceBridge.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ private static ScriptService getScriptService(final Settings settings, final Lon
5353
PainlessScriptEngine.NAME,
5454
new PainlessScriptEngine(settings, scriptContexts),
5555
MustacheScriptEngine.NAME,
56-
new MustacheScriptEngine()
56+
new MustacheScriptEngine(settings)
5757
);
5858
return new ScriptService(settings, scriptEngines, ScriptModule.CORE_CONTEXTS, timeProvider);
5959
}

modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustachePlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class MustachePlugin extends Plugin implements ScriptPlugin, ActionPlugin
4444

4545
@Override
4646
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
47-
return new MustacheScriptEngine();
47+
return new MustacheScriptEngine(settings);
4848
}
4949

5050
@Override

modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/MustacheScriptEngine.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414

1515
import org.apache.logging.log4j.LogManager;
1616
import org.apache.logging.log4j.Logger;
17+
import org.elasticsearch.ElasticsearchParseException;
18+
import org.elasticsearch.ExceptionsHelper;
19+
import org.elasticsearch.common.settings.Setting;
20+
import org.elasticsearch.common.settings.Settings;
21+
import org.elasticsearch.common.text.SizeLimitingStringWriter;
22+
import org.elasticsearch.common.unit.ByteSizeValue;
23+
import org.elasticsearch.common.unit.MemorySizeValue;
1724
import org.elasticsearch.script.GeneralScriptException;
1825
import org.elasticsearch.script.Script;
1926
import org.elasticsearch.script.ScriptContext;
@@ -47,6 +54,19 @@ public final class MustacheScriptEngine implements ScriptEngine {
4754

4855
public static final String NAME = "mustache";
4956

57+
public static final Setting<ByteSizeValue> MUSTACHE_RESULT_SIZE_LIMIT = new Setting<>(
58+
"mustache.max_output_size_bytes",
59+
s -> "1mb",
60+
s -> MemorySizeValue.parseBytesSizeValueOrHeapRatio(s, "mustache.max_output_size_bytes"),
61+
Setting.Property.NodeScope
62+
);
63+
64+
private final int sizeLimit;
65+
66+
public MustacheScriptEngine(Settings settings) {
67+
sizeLimit = (int) MUSTACHE_RESULT_SIZE_LIMIT.get(settings).getBytes();
68+
}
69+
5070
/**
5171
* Compile a template string to (in this case) a Mustache object than can
5272
* later be re-used for execution to fill in missing parameter values.
@@ -118,10 +138,15 @@ private class MustacheExecutableScript extends TemplateScript {
118138

119139
@Override
120140
public String execute() {
121-
final StringWriter writer = new StringWriter();
141+
StringWriter writer = new SizeLimitingStringWriter(sizeLimit);
122142
try {
123143
template.execute(writer, params);
124144
} catch (Exception e) {
145+
// size limit exception can appear at several places in the causal list depending on script & context
146+
if (ExceptionsHelper.unwrap(e, SizeLimitingStringWriter.SizeLimitExceededException.class) != null) {
147+
// don't log, client problem
148+
throw new ElasticsearchParseException("Mustache script result size limit exceeded", e);
149+
}
125150
if (shouldLogException(e)) {
126151
logger.error(() -> format("Error running %s", template), e);
127152
}

modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/CustomMustacheFactoryTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.script.mustache;
1111

12+
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.script.Script;
1314
import org.elasticsearch.script.ScriptEngine;
1415
import org.elasticsearch.script.TemplateScript;
@@ -65,7 +66,7 @@ public void testCreateEncoder() {
6566
}
6667

6768
public void testJsonEscapeEncoder() {
68-
final ScriptEngine engine = new MustacheScriptEngine();
69+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
6970
final Map<String, String> params = randomBoolean() ? Map.of(Script.CONTENT_TYPE_OPTION, JSON_MEDIA_TYPE) : Map.of();
7071

7172
TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);
@@ -75,7 +76,7 @@ public void testJsonEscapeEncoder() {
7576
}
7677

7778
public void testDefaultEncoder() {
78-
final ScriptEngine engine = new MustacheScriptEngine();
79+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
7980
final Map<String, String> params = Map.of(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MEDIA_TYPE);
8081

8182
TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);
@@ -85,7 +86,7 @@ public void testDefaultEncoder() {
8586
}
8687

8788
public void testUrlEncoder() {
88-
final ScriptEngine engine = new MustacheScriptEngine();
89+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
8990
final Map<String, String> params = Map.of(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MEDIA_TYPE);
9091

9192
TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);

modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheScriptEngineTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
88
*/
99
package org.elasticsearch.script.mustache;
1010

11+
import com.github.mustachejava.MustacheException;
1112
import com.github.mustachejava.MustacheFactory;
1213

14+
import org.elasticsearch.ElasticsearchParseException;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.settings.Settings;
17+
import org.elasticsearch.common.text.SizeLimitingStringWriter;
1318
import org.elasticsearch.script.GeneralScriptException;
1419
import org.elasticsearch.script.Script;
1520
import org.elasticsearch.script.TemplateScript;
@@ -24,6 +29,9 @@
2429
import java.util.List;
2530
import java.util.Map;
2631

32+
import static org.elasticsearch.test.LambdaMatchers.transformedMatch;
33+
import static org.hamcrest.Matchers.allOf;
34+
import static org.hamcrest.Matchers.endsWith;
2735
import static org.hamcrest.Matchers.equalTo;
2836
import static org.hamcrest.Matchers.instanceOf;
2937
import static org.hamcrest.Matchers.startsWith;
@@ -37,7 +45,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
3745

3846
@Before
3947
public void setup() {
40-
qe = new MustacheScriptEngine();
48+
qe = new MustacheScriptEngine(Settings.builder().put(MustacheScriptEngine.MUSTACHE_RESULT_SIZE_LIMIT.getKey(), "1kb").build());
4149
factory = CustomMustacheFactory.builder().build();
4250
}
4351

@@ -402,6 +410,24 @@ public void testEscapeJson() throws IOException {
402410
}
403411
}
404412

413+
public void testResultSizeLimit() throws IOException {
414+
String vals = "\"" + "{{val}}".repeat(200) + "\"";
415+
String params = "\"val\":\"aaaaaaaaaa\"";
416+
XContentParser parser = createParser(JsonXContent.jsonXContent, Strings.format("{\"source\":%s,\"params\":{%s}}", vals, params));
417+
Script script = Script.parse(parser);
418+
var compiled = qe.compile(null, script.getIdOrCode(), TemplateScript.CONTEXT, Map.of());
419+
TemplateScript templateScript = compiled.newInstance(script.getParams());
420+
var ex = expectThrows(ElasticsearchParseException.class, templateScript::execute);
421+
assertThat(ex.getCause(), instanceOf(MustacheException.class));
422+
assertThat(
423+
ex.getCause().getCause(),
424+
allOf(
425+
instanceOf(SizeLimitingStringWriter.SizeLimitExceededException.class),
426+
transformedMatch(Throwable::getMessage, endsWith("has exceeded the size limit [1024]"))
427+
)
428+
);
429+
}
430+
405431
private String getChars() {
406432
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
407433
for (int i = 0; i < string.length(); i++) {

modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/MustacheTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package org.elasticsearch.script.mustache;
1010

1111
import org.elasticsearch.common.bytes.BytesReference;
12+
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.common.xcontent.XContentHelper;
1314
import org.elasticsearch.core.Strings;
1415
import org.elasticsearch.script.ScriptEngine;
@@ -39,7 +40,7 @@
3940

4041
public class MustacheTests extends ESTestCase {
4142

42-
private ScriptEngine engine = new MustacheScriptEngine();
43+
private ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
4344

4445
public void testBasics() {
4546
String template = """

qa/smoke-test-ingest-with-all-dependencies/src/yamlRestTest/java/org/elasticsearch/ingest/AbstractScriptTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public abstract class AbstractScriptTestCase extends ESTestCase {
3131

3232
@Before
3333
public void init() throws Exception {
34-
MustacheScriptEngine engine = new MustacheScriptEngine();
34+
MustacheScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
3535
Map<String, ScriptEngine> engines = Collections.singletonMap(engine.getType(), engine);
3636
scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS, () -> 1L);
3737
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.common.text;
11+
12+
import org.elasticsearch.common.Strings;
13+
14+
import java.io.StringWriter;
15+
16+
/**
17+
* A {@link StringWriter} that throws an exception if the string exceeds a specified size.
18+
*/
19+
public class SizeLimitingStringWriter extends StringWriter {
20+
21+
public static class SizeLimitExceededException extends IllegalStateException {
22+
public SizeLimitExceededException(String message) {
23+
super(message);
24+
}
25+
}
26+
27+
private final int sizeLimit;
28+
29+
public SizeLimitingStringWriter(int sizeLimit) {
30+
this.sizeLimit = sizeLimit;
31+
}
32+
33+
private void checkSizeLimit(int additionalChars) {
34+
int bufLen = getBuffer().length();
35+
if (bufLen + additionalChars > sizeLimit) {
36+
throw new SizeLimitExceededException(
37+
Strings.format("String [%s...] has exceeded the size limit [%s]", getBuffer().substring(0, Math.min(bufLen, 20)), sizeLimit)
38+
);
39+
}
40+
}
41+
42+
@Override
43+
public void write(int c) {
44+
checkSizeLimit(1);
45+
super.write(c);
46+
}
47+
48+
// write(char[]) delegates to write(char[], int, int)
49+
50+
@Override
51+
public void write(char[] cbuf, int off, int len) {
52+
checkSizeLimit(len);
53+
super.write(cbuf, off, len);
54+
}
55+
56+
@Override
57+
public void write(String str) {
58+
checkSizeLimit(str.length());
59+
super.write(str);
60+
}
61+
62+
@Override
63+
public void write(String str, int off, int len) {
64+
checkSizeLimit(len);
65+
super.write(str, off, len);
66+
}
67+
68+
// append(...) delegates to write(...) methods
69+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.common.text;
11+
12+
import org.elasticsearch.test.ESTestCase;
13+
14+
public class SizeLimitingStringWriterTests extends ESTestCase {
15+
public void testSizeIsLimited() {
16+
SizeLimitingStringWriter writer = new SizeLimitingStringWriter(10);
17+
18+
writer.write("a".repeat(10));
19+
20+
// test all the methods
21+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write('a'));
22+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write("a"));
23+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write(new char[1]));
24+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write(new char[1], 0, 1));
25+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append('a'));
26+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append("a"));
27+
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append("a", 0, 1));
28+
}
29+
}

0 commit comments

Comments
 (0)