Disable regexes by default in painless

Adds a new node level, non-dynamic setting, `script.painless.regex.enabled`
can be used to enable regexes.

Closes #20397
This commit is contained in:
Nik Everett 2016-09-12 12:06:29 -04:00
parent 119d198cc5
commit 69bf08f6c6
10 changed files with 141 additions and 17 deletions

View File

@ -24,6 +24,9 @@ integTest {
setting 'script.inline', 'true' setting 'script.inline', 'true'
setting 'script.stored', 'true' setting 'script.stored', 'true'
setting 'script.max_compilations_per_minute', '1000' setting 'script.max_compilations_per_minute', '1000'
/* Enable regexes in painless so our tests don't complain about example
* snippets that use them. */
setting 'script.painless.regex.enabled', 'true'
Closure configFile = { Closure configFile = {
extraConfigFile it, "src/test/cluster/config/$it" extraConfigFile it, "src/test/cluster/config/$it"
} }

View File

@ -196,6 +196,15 @@ POST hockey/player/1/_update
[[modules-scripting-painless-regex]] [[modules-scripting-painless-regex]]
=== Regular expressions === Regular expressions
NOTE: Regexes are disabled by default because they circumvent Painless's
protection against long running and memory hungry scripts. To make matters
worse even innocuous looking regexes can have staggering performance and stack
depth behavior. They remain an amazing powerful tool but are too scary to enable
by default. To enable them yourself set `script.painless.regex.enabled: true` in
`elasticsearch.yml`. We'd like very much to have a safe alternative
implementation that can be enabled by default so check this space for later
developments!
Painless's native support for regular expressions has syntax constructs: Painless's native support for regular expressions has syntax constructs:
* `/pattern/`: Pattern literals create patterns. This is the only way to create * `/pattern/`: Pattern literals create patterns. This is the only way to create

View File

@ -19,10 +19,18 @@
package org.elasticsearch.painless; package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
/** /**
* Settings to use when compiling a script. * Settings to use when compiling a script.
*/ */
public final class CompilerSettings { public final class CompilerSettings {
/**
* Are regexes enabled? This is a node level setting because regexes break out of painless's lovely sandbox and can cause stack
* overflows and we can't analyze the regex to be sure it won't.
*/
public static final Setting<Boolean> REGEX_ENABLED = Setting.boolSetting("script.painless.regex.enabled", false, Property.NodeScope);
/** /**
* Constant to be used when specifying the maximum loop counter when compiling a script. * Constant to be used when specifying the maximum loop counter when compiling a script.
@ -55,6 +63,12 @@ public final class CompilerSettings {
*/ */
private int initialCallSiteDepth = 0; private int initialCallSiteDepth = 0;
/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
*/
private boolean regexesEnabled = false;
/** /**
* Returns the value for the cumulative total number of statements that can be made in all loops * Returns the value for the cumulative total number of statements that can be made in all loops
* in a script before an exception is thrown. This attempts to prevent infinite loops. Note if * in a script before an exception is thrown. This attempts to prevent infinite loops. Note if
@ -104,4 +118,20 @@ public final class CompilerSettings {
public void setInitialCallSiteDepth(int depth) { public void setInitialCallSiteDepth(int depth) {
this.initialCallSiteDepth = depth; this.initialCallSiteDepth = depth;
} }
/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
*/
public boolean areRegexesEnabled() {
return regexesEnabled;
}
/**
* Are regexes enabled? They are currently disabled by default because they break out of the loop counter and even fairly simple
* <strong>looking</strong> regexes can cause stack overflows.
*/
public void setRegexesEnabled(boolean regexesEnabled) {
this.regexesEnabled = regexesEnabled;
}
} }

View File

@ -20,19 +20,21 @@
package org.elasticsearch.painless; package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.plugins.Plugin; import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin; import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.script.ScriptEngineRegistry;
import org.elasticsearch.script.ScriptEngineService; import org.elasticsearch.script.ScriptEngineService;
import org.elasticsearch.script.ScriptModule;
import java.util.Arrays;
import java.util.List;
/** /**
* Registers Painless as a plugin. * Registers Painless as a plugin.
*/ */
public final class PainlessPlugin extends Plugin implements ScriptPlugin { public final class PainlessPlugin extends Plugin implements ScriptPlugin {
// force to pare our definition at startup (not on the user's first script) // force to parse our definition at startup (not on the user's first script)
static { static {
Definition.VOID_TYPE.hashCode(); Definition.VOID_TYPE.hashCode();
} }
@ -41,4 +43,9 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin {
public ScriptEngineService getScriptEngineService(Settings settings) { public ScriptEngineService getScriptEngineService(Settings settings) {
return new PainlessScriptEngineService(settings); return new PainlessScriptEngineService(settings);
} }
@Override
public List<Setting<?>> getSettings() {
return Arrays.asList(CompilerSettings.REGEX_ENABLED);
}
} }

View File

@ -53,11 +53,6 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
*/ */
public static final String NAME = "painless"; public static final String NAME = "painless";
/**
* Default compiler settings to be used.
*/
private static final CompilerSettings DEFAULT_COMPILER_SETTINGS = new CompilerSettings();
/** /**
* Permissions context used during compilation. * Permissions context used during compilation.
*/ */
@ -74,12 +69,19 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
}); });
} }
/**
* Default compiler settings to be used. Note that {@link CompilerSettings} is mutable but this instance shouldn't be mutated outside
* of {@link PainlessScriptEngineService#PainlessScriptEngineService(Settings)}.
*/
private final CompilerSettings defaultCompilerSettings = new CompilerSettings();
/** /**
* Constructor. * Constructor.
* @param settings The settings to initialize the engine with. * @param settings The settings to initialize the engine with.
*/ */
public PainlessScriptEngineService(final Settings settings) { public PainlessScriptEngineService(final Settings settings) {
super(settings); super(settings);
defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings));
} }
/** /**
@ -111,29 +113,36 @@ public final class PainlessScriptEngineService extends AbstractComponent impleme
if (params.isEmpty()) { if (params.isEmpty()) {
// Use the default settings. // Use the default settings.
compilerSettings = DEFAULT_COMPILER_SETTINGS; compilerSettings = defaultCompilerSettings;
} else { } else {
// Use custom settings specified by params. // Use custom settings specified by params.
compilerSettings = new CompilerSettings(); compilerSettings = new CompilerSettings();
Map<String, String> copy = new HashMap<>(params);
String value = copy.remove(CompilerSettings.MAX_LOOP_COUNTER);
// Except regexes enabled - this is a node level setting and can't be changed in the request.
compilerSettings.setRegexesEnabled(defaultCompilerSettings.areRegexesEnabled());
Map<String, String> copy = new HashMap<>(params);
String value = copy.remove(CompilerSettings.MAX_LOOP_COUNTER);
if (value != null) { if (value != null) {
compilerSettings.setMaxLoopCounter(Integer.parseInt(value)); compilerSettings.setMaxLoopCounter(Integer.parseInt(value));
} }
value = copy.remove(CompilerSettings.PICKY); value = copy.remove(CompilerSettings.PICKY);
if (value != null) { if (value != null) {
compilerSettings.setPicky(Boolean.parseBoolean(value)); compilerSettings.setPicky(Boolean.parseBoolean(value));
} }
value = copy.remove(CompilerSettings.INITIAL_CALL_SITE_DEPTH); value = copy.remove(CompilerSettings.INITIAL_CALL_SITE_DEPTH);
if (value != null) { if (value != null) {
compilerSettings.setInitialCallSiteDepth(Integer.parseInt(value)); compilerSettings.setInitialCallSiteDepth(Integer.parseInt(value));
} }
value = copy.remove(CompilerSettings.REGEX_ENABLED.getKey());
if (value != null) {
throw new IllegalArgumentException("[painless.regex.enabled] can only be set on node startup.");
}
if (!copy.isEmpty()) { if (!copy.isEmpty()) {
throw new IllegalArgumentException("Unrecognized compile-time parameter(s): " + copy); throw new IllegalArgumentException("Unrecognized compile-time parameter(s): " + copy);
} }

View File

@ -796,6 +796,11 @@ public final class Walker extends PainlessParserBaseVisitor<ANode> {
@Override @Override
public ANode visitRegex(RegexContext ctx) { public ANode visitRegex(RegexContext ctx) {
if (false == settings.areRegexesEnabled()) {
throw location(ctx).createError(new IllegalStateException("Regexes are disabled. Set [script.painless.regex.enabled] to [true] "
+ "in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep "
+ "recursion and long loops."));
}
String text = ctx.REGEX().getText(); String text = ctx.REGEX().getText();
int lastSlash = text.lastIndexOf('/'); int lastSlash = text.lastIndexOf('/');
String pattern = text.substring(1, lastSlash); String pattern = text.substring(1, lastSlash);

View File

@ -19,17 +19,26 @@
package org.elasticsearch.painless; package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Settings;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet; import java.util.HashSet;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException; import java.util.regex.PatternSyntaxException;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
public class RegexTests extends ScriptTestCase { public class RegexTests extends ScriptTestCase {
@Override
protected Settings scriptEngineSettings() {
// Enable regexes just for this test. They are disabled by default.
return Settings.builder()
.put(CompilerSettings.REGEX_ENABLED.getKey(), true)
.build();
}
public void testPatternAfterReturn() { public void testPatternAfterReturn() {
assertEquals(true, exec("return 'foo' ==~ /foo/")); assertEquals(true, exec("return 'foo' ==~ /foo/"));
assertEquals(false, exec("return 'bar' ==~ /foo/")); assertEquals(false, exec("return 'bar' ==~ /foo/"));

View File

@ -45,7 +45,14 @@ public abstract class ScriptTestCase extends ESTestCase {
@Before @Before
public void setup() { public void setup() {
scriptEngine = new PainlessScriptEngineService(Settings.EMPTY); scriptEngine = new PainlessScriptEngineService(scriptEngineSettings());
}
/**
* Settings used to build the script engine. Override to customize settings like {@link RegexTests} does to enable regexes.
*/
protected Settings scriptEngineSettings() {
return Settings.EMPTY;
} }
/** Compiles and returns the result of {@code script} */ /** Compiles and returns the result of {@code script} */
@ -71,6 +78,7 @@ public abstract class ScriptTestCase extends ESTestCase {
if (picky) { if (picky) {
CompilerSettings pickySettings = new CompilerSettings(); CompilerSettings pickySettings = new CompilerSettings();
pickySettings.setPicky(true); pickySettings.setPicky(true);
pickySettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(scriptEngineSettings()));
Walker.buildPainlessTree(getTestName(), script, pickySettings, null); Walker.buildPainlessTree(getTestName(), script, pickySettings, null);
} }
// test actual script execution // test actual script execution

View File

@ -20,14 +20,13 @@
package org.elasticsearch.painless; package org.elasticsearch.painless;
import org.apache.lucene.util.Constants; import org.apache.lucene.util.Constants;
import org.elasticsearch.script.ScriptException;
import java.lang.invoke.WrongMethodTypeException; import java.lang.invoke.WrongMethodTypeException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import static java.util.Collections.emptyMap; import static java.util.Collections.emptyMap;
import static org.hamcrest.Matchers.containsString; import static java.util.Collections.singletonMap;
public class WhenThingsGoWrongTests extends ScriptTestCase { public class WhenThingsGoWrongTests extends ScriptTestCase {
public void testNullPointer() { public void testNullPointer() {
@ -234,4 +233,16 @@ public class WhenThingsGoWrongTests extends ScriptTestCase {
exec("void recurse(int x, int y) {recurse(x, y)} recurse(1, 2);"); exec("void recurse(int x, int y) {recurse(x, y)} recurse(1, 2);");
}); });
} }
public void testRegexDisabledByDefault() {
IllegalStateException e = expectThrows(IllegalStateException.class, () -> exec("return 'foo' ==~ /foo/"));
assertEquals("Regexes are disabled. Set [script.painless.regex.enabled] to [true] in elasticsearch.yaml to allow them. "
+ "Be careful though, regexes break out of Painless's protection against deep recursion and long loops.", e.getMessage());
}
public void testCanNotOverrideRegexEnabled() {
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
() -> exec("", null, singletonMap(CompilerSettings.REGEX_ENABLED.getKey(), "true"), null, false));
assertEquals("[painless.regex.enabled] can only be set on node startup.", e.getMessage());
}
} }

View File

@ -0,0 +1,33 @@
---
"Regex in update fails":
- do:
index:
index: test_1
type: test
id: 1
body:
foo: bar
count: 1
- do:
catch: /Regexes are disabled. Set \[script.painless.regex.enabled\] to \[true\] in elasticsearch.yaml to allow them. Be careful though, regexes break out of Painless's protection against deep recursion and long loops./
update:
index: test_1
type: test
id: 1
body:
script:
lang: painless
inline: "ctx._source.foo = params.bar ==~ /cat/"
params: { bar: 'xxx' }
---
"Regex enabled is not a dynamic setting":
- do:
catch: /setting \[script.painless.regex.enabled\], not dynamically updateable/
cluster.put_settings:
body:
transient:
script.painless.regex.enabled: true