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:
parent
119d198cc5
commit
69bf08f6c6
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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/"));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue