Skip to content

Commit f9b6b57

Browse files
authored
[7.17] Add a size limit to outputs from mustache (#114002) (#114705)
Backport #114002 to 7.17
1 parent 9eddf32 commit f9b6b57

File tree

16 files changed

+149
-21
lines changed

16 files changed

+149
-21
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: []

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
@@ -34,7 +34,7 @@ public class MustachePlugin extends Plugin implements ScriptPlugin, ActionPlugin
3434

3535
@Override
3636
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
37-
return new MustacheScriptEngine();
37+
return new MustacheScriptEngine(settings);
3838
}
3939

4040
@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
@@ -15,7 +15,14 @@
1515
import org.apache.logging.log4j.Logger;
1616
import org.apache.logging.log4j.message.ParameterizedMessage;
1717
import org.apache.logging.log4j.util.Supplier;
18+
import org.elasticsearch.ElasticsearchParseException;
19+
import org.elasticsearch.ExceptionsHelper;
1820
import org.elasticsearch.SpecialPermission;
21+
import org.elasticsearch.common.settings.Setting;
22+
import org.elasticsearch.common.settings.Settings;
23+
import org.elasticsearch.common.text.SizeLimitingStringWriter;
24+
import org.elasticsearch.common.unit.ByteSizeValue;
25+
import org.elasticsearch.common.unit.MemorySizeValue;
1926
import org.elasticsearch.script.GeneralScriptException;
2027
import org.elasticsearch.script.Script;
2128
import org.elasticsearch.script.ScriptContext;
@@ -45,6 +52,19 @@ public final class MustacheScriptEngine implements ScriptEngine {
4552

4653
public static final String NAME = "mustache";
4754

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

107127
@Override
108128
public String execute() {
109-
final StringWriter writer = new StringWriter();
129+
final StringWriter writer = new SizeLimitingStringWriter(sizeLimit);
110130
try {
111131
// crazy reflection here
112132
SpecialPermission.check();
@@ -115,6 +135,11 @@ public String execute() {
115135
return null;
116136
});
117137
} catch (Exception e) {
138+
// size limit exception can appear at several places in the causal list depending on script & context
139+
if (ExceptionsHelper.unwrap(e, SizeLimitingStringWriter.SizeLimitExceededException.class) != null) {
140+
// don't log, client problem
141+
throw new ElasticsearchParseException("Mustache script result size limit exceeded", e);
142+
}
118143
logger.error((Supplier<?>) () -> new ParameterizedMessage("Error running {}", template), e);
119144
throw new GeneralScriptException("Error running " + template, e);
120145
}

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
@@ -8,6 +8,7 @@
88

99
package org.elasticsearch.script.mustache;
1010

11+
import org.elasticsearch.common.settings.Settings;
1112
import org.elasticsearch.script.Script;
1213
import org.elasticsearch.script.ScriptEngine;
1314
import org.elasticsearch.script.TemplateScript;
@@ -54,7 +55,7 @@ public void testCreateEncoder() {
5455
}
5556

5657
public void testJsonEscapeEncoder() {
57-
final ScriptEngine engine = new MustacheScriptEngine();
58+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
5859
final Map<String, String> params = randomBoolean() ? singletonMap(Script.CONTENT_TYPE_OPTION, JSON_MIME_TYPE) : emptyMap();
5960

6061
TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);
@@ -64,7 +65,7 @@ public void testJsonEscapeEncoder() {
6465
}
6566

6667
public void testDefaultEncoder() {
67-
final ScriptEngine engine = new MustacheScriptEngine();
68+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
6869
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MIME_TYPE);
6970

7071
TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);
@@ -74,7 +75,7 @@ public void testDefaultEncoder() {
7475
}
7576

7677
public void testUrlEncoder() {
77-
final ScriptEngine engine = new MustacheScriptEngine();
78+
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
7879
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MIME_TYPE);
7980

8081
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.github.mustachejava.MustacheFactory;
1111

12+
import org.elasticsearch.common.settings.Settings;
1213
import org.elasticsearch.script.Script;
1314
import org.elasticsearch.script.TemplateScript;
1415
import org.elasticsearch.test.ESTestCase;
@@ -33,7 +34,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
3334

3435
@Before
3536
public void setup() {
36-
qe = new MustacheScriptEngine();
37+
qe = new MustacheScriptEngine(Settings.EMPTY);
3738
factory = new CustomMustacheFactory();
3839
}
3940

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
@@ -8,6 +8,7 @@
88
package org.elasticsearch.script.mustache;
99

1010
import org.elasticsearch.common.bytes.BytesReference;
11+
import org.elasticsearch.common.settings.Settings;
1112
import org.elasticsearch.common.xcontent.XContentHelper;
1213
import org.elasticsearch.script.ScriptEngine;
1314
import org.elasticsearch.script.ScriptException;
@@ -38,7 +39,7 @@
3839

3940
public class MustacheTests extends ESTestCase {
4041

41-
private ScriptEngine engine = new MustacheScriptEngine();
42+
private ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
4243

4344
public void testBasics() {
4445
String template = "GET _search {\"query\": "

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
@@ -30,7 +30,7 @@ public abstract class AbstractScriptTestCase extends ESTestCase {
3030

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

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/support/mapper/TemplateRoleNameTests.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public void testEqualsAndHashCode() throws Exception {
9393
public void testEvaluateRoles() throws Exception {
9494
final ScriptService scriptService = new ScriptService(
9595
Settings.EMPTY,
96-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
96+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
9797
ScriptModule.CORE_CONTEXTS
9898
);
9999
final ExpressionModel model = new ExpressionModel();
@@ -149,7 +149,7 @@ public void tryEquals(TemplateRoleName original) {
149149
public void testValidate() {
150150
final ScriptService scriptService = new ScriptService(
151151
Settings.EMPTY,
152-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
152+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
153153
ScriptModule.CORE_CONTEXTS
154154
);
155155

@@ -175,7 +175,7 @@ public void testValidate() {
175175
public void testValidateWillPassWithEmptyContext() {
176176
final ScriptService scriptService = new ScriptService(
177177
Settings.EMPTY,
178-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
178+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
179179
ScriptModule.CORE_CONTEXTS
180180
);
181181

@@ -205,7 +205,7 @@ public void testValidateWillPassWithEmptyContext() {
205205
public void testValidateWillFailForSyntaxError() {
206206
final ScriptService scriptService = new ScriptService(
207207
Settings.EMPTY,
208-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
208+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
209209
ScriptModule.CORE_CONTEXTS
210210
);
211211

@@ -267,7 +267,7 @@ public void testValidationWillFailWhenInlineScriptIsNotEnabled() {
267267
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
268268
final ScriptService scriptService = new ScriptService(
269269
settings,
270-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
270+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
271271
ScriptModule.CORE_CONTEXTS
272272
);
273273
final BytesReference inlineScript = new BytesArray("{ \"source\":\"\" }");
@@ -282,7 +282,7 @@ public void testValidateWillFailWhenStoredScriptIsNotEnabled() {
282282
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
283283
final ScriptService scriptService = new ScriptService(
284284
settings,
285-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
285+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
286286
ScriptModule.CORE_CONTEXTS
287287
);
288288
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);
@@ -309,7 +309,7 @@ public void testValidateWillFailWhenStoredScriptIsNotEnabled() {
309309
public void testValidateWillFailWhenStoredScriptIsNotFound() {
310310
final ScriptService scriptService = new ScriptService(
311311
Settings.EMPTY,
312-
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
312+
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
313313
ScriptModule.CORE_CONTEXTS
314314
);
315315
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);

0 commit comments

Comments
 (0)