Skip to content

Commit

Permalink
[7.17] Add a size limit to outputs from mustache (#114002) (#114705)
Browse files Browse the repository at this point in the history
Backport #114002 to 7.17
  • Loading branch information
thecoop authored Oct 14, 2024
1 parent 9eddf32 commit f9b6b57
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 21 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/114002.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 114002
summary: Add a `mustache.max_output_size_bytes` setting to limit the length of results from mustache scripts
area: Infra/Scripting
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class MustachePlugin extends Plugin implements ScriptPlugin, ActionPlugin

@Override
public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
return new MustacheScriptEngine();
return new MustacheScriptEngine(settings);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.SizeLimitingStringWriter;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.MemorySizeValue;
import org.elasticsearch.script.GeneralScriptException;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
Expand Down Expand Up @@ -45,6 +52,19 @@ public final class MustacheScriptEngine implements ScriptEngine {

public static final String NAME = "mustache";

public static final Setting<ByteSizeValue> MUSTACHE_RESULT_SIZE_LIMIT = new Setting<>(
"mustache.max_output_size_bytes",
s -> "1mb",
s -> MemorySizeValue.parseBytesSizeValueOrHeapRatio(s, "mustache.max_output_size_bytes"),
Setting.Property.NodeScope
);

private final int sizeLimit;

public MustacheScriptEngine(Settings settings) {
sizeLimit = (int) MUSTACHE_RESULT_SIZE_LIMIT.get(settings).getBytes();
}

/**
* Compile a template string to (in this case) a Mustache object than can
* later be re-used for execution to fill in missing parameter values.
Expand Down Expand Up @@ -106,7 +126,7 @@ private class MustacheExecutableScript extends TemplateScript {

@Override
public String execute() {
final StringWriter writer = new StringWriter();
final StringWriter writer = new SizeLimitingStringWriter(sizeLimit);
try {
// crazy reflection here
SpecialPermission.check();
Expand All @@ -115,6 +135,11 @@ public String execute() {
return null;
});
} catch (Exception e) {
// size limit exception can appear at several places in the causal list depending on script & context
if (ExceptionsHelper.unwrap(e, SizeLimitingStringWriter.SizeLimitExceededException.class) != null) {
// don't log, client problem
throw new ElasticsearchParseException("Mustache script result size limit exceeded", e);
}
logger.error((Supplier<?>) () -> new ParameterizedMessage("Error running {}", template), e);
throw new GeneralScriptException("Error running " + template, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.elasticsearch.script.mustache;

import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.TemplateScript;
Expand Down Expand Up @@ -54,7 +55,7 @@ public void testCreateEncoder() {
}

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

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

public void testDefaultEncoder() {
final ScriptEngine engine = new MustacheScriptEngine();
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MIME_TYPE);

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

public void testUrlEncoder() {
final ScriptEngine engine = new MustacheScriptEngine();
final ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MIME_TYPE);

TemplateScript.Factory compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", TemplateScript.CONTEXT, params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import com.github.mustachejava.MustacheFactory;

import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.TemplateScript;
import org.elasticsearch.test.ESTestCase;
Expand All @@ -33,7 +34,7 @@ public class MustacheScriptEngineTests extends ESTestCase {

@Before
public void setup() {
qe = new MustacheScriptEngine();
qe = new MustacheScriptEngine(Settings.EMPTY);
factory = new CustomMustacheFactory();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.elasticsearch.script.mustache;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.ScriptException;
Expand Down Expand Up @@ -38,7 +39,7 @@

public class MustacheTests extends ESTestCase {

private ScriptEngine engine = new MustacheScriptEngine();
private ScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);

public void testBasics() {
String template = "GET _search {\"query\": "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public abstract class AbstractScriptTestCase extends ESTestCase {

@Before
public void init() throws Exception {
MustacheScriptEngine engine = new MustacheScriptEngine();
MustacheScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
Map<String, ScriptEngine> engines = Collections.singletonMap(engine.getType(), engine);
scriptService = new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.common.text;

import java.io.StringWriter;
import java.util.Locale;

/**
* A {@link StringWriter} that throws an exception if the string exceeds a specified size.
*/
public class SizeLimitingStringWriter extends StringWriter {

public static class SizeLimitExceededException extends IllegalStateException {
public SizeLimitExceededException(String message) {
super(message);
}
}

private final int sizeLimit;

public SizeLimitingStringWriter(int sizeLimit) {
this.sizeLimit = sizeLimit;
}

private void checkSizeLimit(int additionalChars) {
int bufLen = getBuffer().length();
if (bufLen + additionalChars > sizeLimit) {
String substring = getBuffer().substring(0, Math.min(bufLen, 20));
throw new SizeLimitExceededException(
String.format(Locale.ROOT, "String [%s...] has exceeded the size limit [%s]", substring, sizeLimit)
);
}
}

@Override
public void write(int c) {
checkSizeLimit(1);
super.write(c);
}

// write(char[]) delegates to write(char[], int, int)

@Override
public void write(char[] cbuf, int off, int len) {
checkSizeLimit(len);
super.write(cbuf, off, len);
}

@Override
public void write(String str) {
checkSizeLimit(str.length());
super.write(str);
}

@Override
public void write(String str, int off, int len) {
checkSizeLimit(len);
super.write(str, off, len);
}

// append(...) delegates to write(...) methods
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
package org.elasticsearch.common.text;

import org.elasticsearch.test.ESTestCase;

public class SizeLimitingStringWriterTests extends ESTestCase {
public void testSizeIsLimited() {
SizeLimitingStringWriter writer = new SizeLimitingStringWriter(10);

writer.write("aaaaaaaaaa");

// test all the methods
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write('a'));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write("a"));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write(new char[1]));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.write(new char[1], 0, 1));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append('a'));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append("a"));
expectThrows(SizeLimitingStringWriter.SizeLimitExceededException.class, () -> writer.append("a", 0, 1));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void testEqualsAndHashCode() throws Exception {
public void testEvaluateRoles() throws Exception {
final ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
final ExpressionModel model = new ExpressionModel();
Expand Down Expand Up @@ -149,7 +149,7 @@ public void tryEquals(TemplateRoleName original) {
public void testValidate() {
final ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);

Expand All @@ -175,7 +175,7 @@ public void testValidate() {
public void testValidateWillPassWithEmptyContext() {
final ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);

Expand Down Expand Up @@ -205,7 +205,7 @@ public void testValidateWillPassWithEmptyContext() {
public void testValidateWillFailForSyntaxError() {
final ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);

Expand Down Expand Up @@ -267,7 +267,7 @@ public void testValidationWillFailWhenInlineScriptIsNotEnabled() {
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
final ScriptService scriptService = new ScriptService(
settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
final BytesReference inlineScript = new BytesArray("{ \"source\":\"\" }");
Expand All @@ -282,7 +282,7 @@ public void testValidateWillFailWhenStoredScriptIsNotEnabled() {
final Settings settings = Settings.builder().put("script.allowed_types", ScriptService.ALLOW_NONE).build();
final ScriptService scriptService = new ScriptService(
settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);
Expand All @@ -309,7 +309,7 @@ public void testValidateWillFailWhenStoredScriptIsNotEnabled() {
public void testValidateWillFailWhenStoredScriptIsNotFound() {
final ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
final ClusterChangedEvent clusterChangedEvent = mock(ClusterChangedEvent.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.elasticsearch.xpack.core.security.authz.support;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptService;
Expand Down Expand Up @@ -101,7 +102,7 @@ public void testDocLevelSecurityTemplateWithOpenIdConnectStyleMetadata() throws
true
);

final MustacheScriptEngine mustache = new MustacheScriptEngine();
final MustacheScriptEngine mustache = new MustacheScriptEngine(Settings.EMPTY);

when(scriptService.compile(any(Script.class), eq(TemplateScript.CONTEXT))).thenAnswer(inv -> {
assertThat(inv.getArguments(), arrayWithSize(2));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void setUpResolver() {
final Settings settings = Settings.EMPTY;
final ScriptService scriptService = new ScriptService(
settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
final ServiceProviderDefaults samlDefaults = new ServiceProviderDefaults(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ public void testRealmWithTemplatedRoleMapping() throws Exception {

final ScriptService scriptService = new ScriptService(
settings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
NativeRoleMappingStore roleMapper = new NativeRoleMappingStore(settings, mockClient, mockSecurityIndex, scriptService) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ public void testLdapRealmWithTemplatedRoleMapping() throws Exception {

final ScriptService scriptService = new ScriptService(
defaultGlobalSettings,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
NativeRoleMappingStore roleMapper = new NativeRoleMappingStore(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public void testResolveRoles() throws Exception {
SecurityIndexManager securityIndex = mock(SecurityIndexManager.class);
ScriptService scriptService = new ScriptService(
Settings.EMPTY,
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine()),
Collections.singletonMap(MustacheScriptEngine.NAME, new MustacheScriptEngine(Settings.EMPTY)),
ScriptModule.CORE_CONTEXTS
);
when(securityIndex.isAvailable()).thenReturn(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class WatcherTemplateTests extends ESTestCase {

@Before
public void init() throws Exception {
MustacheScriptEngine engine = new MustacheScriptEngine();
MustacheScriptEngine engine = new MustacheScriptEngine(Settings.EMPTY);
Map<String, ScriptEngine> engines = Collections.singletonMap(engine.getType(), engine);
Map<String, ScriptContext<?>> contexts = Collections.singletonMap(
Watcher.SCRIPT_TEMPLATE_CONTEXT.name,
Expand Down

0 comments on commit f9b6b57

Please sign in to comment.