Skip to content

Commit

Permalink
Add Groovy as a scripting language, add sandboxing for Groovy
Browse files Browse the repository at this point in the history
Sandboxes the groovy scripting language with multiple configurable
whitelists:

`script.groovy.sandbox.receiver_whitelist`: comma-separated list of string
classes for objects that may have methods invoked.
`script.groovy.sandbox.package_whitelist`: comma-separated list of
packages under which new objects may be constructed.
`script.groovy.sandbox.class_whitelist` comma-separated list of classes
that are allowed to be constructed.

As well as a method blacklist:

`script.groovy.sandbox.method_blacklist`: comma-separated list of
methods that are never allowed to be invoked, regardless of target
object.

The sandbox can be entirely disabled by setting:

`script.groovy.sandbox.enabled: false`
  • Loading branch information
dakrone committed Jun 20, 2014
1 parent 12fd6ce commit c70f6d0
Show file tree
Hide file tree
Showing 21 changed files with 738 additions and 59 deletions.
5 changes: 3 additions & 2 deletions dev-tools/tests.policy
Expand Up @@ -27,10 +27,11 @@ grant {
permission java.io.FilePermission "${junit4.childvm.cwd}", "read,execute,write";
permission java.io.FilePermission "${junit4.childvm.cwd}${/}-", "read,execute,write,delete";
permission java.io.FilePermission "${junit4.tempDir}${/}*", "read,execute,write,delete";

permission groovy.security.GroovyCodeSourcePermission "/groovy/script";

// Allow connecting to the internet anywhere
permission java.net.SocketPermission "*", "accept,listen,connect,resolve";

// Basic permissions needed for Lucene / Elasticsearch to work:
permission java.util.PropertyPermission "*", "read,write";
permission java.lang.reflect.ReflectPermission "*";
Expand Down
36 changes: 21 additions & 15 deletions pom.xml
Expand Up @@ -208,13 +208,6 @@
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.2.0.Final</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
Expand Down Expand Up @@ -265,6 +258,22 @@
</dependency>
<!-- END: dependencies that are shaded -->

<dependency>
<groupId>org.mvel</groupId>
<artifactId>mvel2</artifactId>
<version>2.2.0.Final</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.3.2</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
Expand Down Expand Up @@ -648,7 +657,6 @@
<includes>
<include>com.google.guava:guava</include>
<include>com.carrotsearch:hppc</include>
<include>org.mvel:mvel2</include>
<include>com.fasterxml.jackson.core:jackson-core</include>
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-smile</include>
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-yaml</include>
Expand All @@ -674,10 +682,6 @@
<pattern>jsr166e</pattern>
<shadedPattern>org.elasticsearch.common.util.concurrent.jsr166e</shadedPattern>
</relocation>
<relocation>
<pattern>org.mvel2</pattern>
<shadedPattern>org.elasticsearch.common.mvel2</shadedPattern>
</relocation>
<relocation>
<pattern>com.fasterxml.jackson</pattern>
<shadedPattern>org.elasticsearch.common.jackson</shadedPattern>
Expand Down Expand Up @@ -870,7 +874,7 @@
</data>
<data>
<src>${project.build.directory}/lib</src>
<includes>lucene*, log4j*, jna*, spatial4j*, jts*</includes>
<includes>lucene*, log4j*, jna*, spatial4j*, jts*, groovy*, mvel*</includes>
<type>directory</type>
<mapper>
<type>perm</type>
Expand Down Expand Up @@ -1070,6 +1074,8 @@
<include>jna*</include>
<include>spatial4j*</include>
<include>jts*</include>
<include>groovy*</include>
<include>mvel*</include>
</includes>
</source>
<source>
Expand Down Expand Up @@ -1393,7 +1399,7 @@
<version>0.6.4.201312101107</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencies>
<build>
<plugins>
<plugin>
Expand All @@ -1418,7 +1424,7 @@
<id>default-check</id>
<goals>
<goal>check</goal>
</goals>
</goals>
</execution>
</executions>
<configuration>
Expand Down
2 changes: 2 additions & 0 deletions src/main/assemblies/common-bin.xml
Expand Up @@ -9,6 +9,8 @@
<include>net.java.dev.jna:jna</include>
<include>com.spatial4j:spatial4j</include>
<include>com.vividsolutions:jts</include>
<include>org.codehaus.groovy:groovy-all</include>
<include>org.mvel:mvel2</include>
</includes>
</dependencySet>
<dependencySet>
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/org/elasticsearch/script/ScriptModule.java
Expand Up @@ -27,6 +27,7 @@
import org.elasticsearch.common.inject.multibindings.Multibinder;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.groovy.GroovyScriptEngineService;
import org.elasticsearch.script.mustache.MustacheScriptEngineService;
import org.elasticsearch.script.mvel.MvelScriptEngineService;

Expand Down Expand Up @@ -77,16 +78,23 @@ protected void configure() {

Multibinder<ScriptEngineService> multibinder = Multibinder.newSetBinder(binder(), ScriptEngineService.class);
multibinder.addBinding().to(NativeScriptEngineService.class);

try {
multibinder.addBinding().to(GroovyScriptEngineService.class);
} catch (Throwable t) {
Loggers.getLogger(GroovyScriptEngineService.class).debug("failed to load groovy", t);
}

try {
multibinder.addBinding().to(MvelScriptEngineService.class);
} catch (Throwable t) {
// no MVEL
Loggers.getLogger(MvelScriptEngineService.class).debug("failed to load mvel", t);
}

try {
multibinder.addBinding().to(MustacheScriptEngineService.class);
} catch (Throwable t) {
Loggers.getLogger(MustacheScriptEngineService.class).trace("failed to load mustache", t);
Loggers.getLogger(MustacheScriptEngineService.class).debug("failed to load mustache", t);
}

for (Class<? extends ScriptEngineService> scriptEngine : scriptEngines) {
Expand Down
56 changes: 48 additions & 8 deletions src/main/java/org/elasticsearch/script/ScriptService.java
Expand Up @@ -44,6 +44,7 @@
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
Expand All @@ -54,6 +55,10 @@
*/
public class ScriptService extends AbstractComponent {

public static final String DEFAULT_SCRIPTING_LANGUAGE_SETTING = "script.default_lang";
public static final String DISABLE_DYNAMIC_SCRIPTING_SETTING = "script.disable_dynamic";
public static final String DISABLE_DYNAMIC_SCRIPTING_DEFAULT = "sandbox";

private final String defaultLang;

private final ImmutableMap<String, ScriptEngineService> scriptEngines;
Expand All @@ -63,7 +68,38 @@ public class ScriptService extends AbstractComponent {
private final Cache<CacheKey, CompiledScript> cache;
private final File scriptsDirectory;

private final boolean disableDynamic;
private final DynamicScriptDisabling dynamicScriptingDisabled;

/**
* Enum defining the different dynamic settings for scripting, either
* ONLY_DISK_ALLOWED (scripts must be placed on disk), EVERYTHING_ALLOWED
* (all dynamic scripting is enabled), or SANDBOXED_ONLY (only sandboxed
* scripting languages are allowed)
*/
enum DynamicScriptDisabling {
EVERYTHING_ALLOWED,
ONLY_DISK_ALLOWED,
SANDBOXED_ONLY;

public static final DynamicScriptDisabling parse(String s) {
switch (s.toLowerCase(Locale.ROOT)) {
// true for "disable_dynamic" means only on-disk scripts are enabled
case "true":
case "all":
return ONLY_DISK_ALLOWED;
// false for "disable_dynamic" means all scripts are enabled
case "false":
case "none":
return EVERYTHING_ALLOWED;
// only sandboxed scripting is enabled
case "sandbox":
case "sandboxed":
return SANDBOXED_ONLY;
default:
throw new ElasticsearchIllegalArgumentException("Unrecognized script allowance setting: [" + s + "]");
}
}
}

@Inject
public ScriptService(Settings settings, Environment env, Set<ScriptEngineService> scriptEngines,
Expand All @@ -74,8 +110,8 @@ public ScriptService(Settings settings, Environment env, Set<ScriptEngineService
TimeValue cacheExpire = componentSettings.getAsTime("cache.expire", null);
logger.debug("using script cache with max_size [{}], expire [{}]", cacheMaxSize, cacheExpire);

this.defaultLang = componentSettings.get("default_lang", "mvel");
this.disableDynamic = componentSettings.getAsBoolean("disable_dynamic", true);
this.defaultLang = settings.get(DEFAULT_SCRIPTING_LANGUAGE_SETTING, "mvel");
this.dynamicScriptingDisabled = DynamicScriptDisabling.parse(settings.get(DISABLE_DYNAMIC_SCRIPTING_SETTING, DISABLE_DYNAMIC_SCRIPTING_DEFAULT));

CacheBuilder cacheBuilder = CacheBuilder.newBuilder();
if (cacheMaxSize >= 0) {
Expand Down Expand Up @@ -130,7 +166,7 @@ public CompiledScript compile(String lang, String script) {
lang = defaultLang;
}
if (!dynamicScriptEnabled(lang)) {
throw new ScriptException("dynamic scripting disabled");
throw new ScriptException("dynamic scripting for [" + lang + "] disabled");
}
CacheKey cacheKey = new CacheKey(lang, script);
compiled = cache.getIfPresent(cacheKey);
Expand Down Expand Up @@ -180,12 +216,16 @@ private boolean dynamicScriptEnabled(String lang) {
if (service == null) {
throw new ElasticsearchIllegalArgumentException("script_lang not supported [" + lang + "]");
}
// Templating languages and native scripts are always allowed
// "native" executions are registered through plugins
if (service.sandboxed() || "native".equals(lang)) {

// Templating languages (mustache) and native scripts are always
// allowed, "native" executions are registered through plugins
if (this.dynamicScriptingDisabled == DynamicScriptDisabling.EVERYTHING_ALLOWED || "native".equals(lang) || "mustache".equals(lang)) {
return true;
} else if (this.dynamicScriptingDisabled == DynamicScriptDisabling.ONLY_DISK_ALLOWED) {
return false;
} else {
return service.sandboxed();
}
return !disableDynamic;
}

private class ScriptChangesListener extends FileChangesListener {
Expand Down

0 comments on commit c70f6d0

Please sign in to comment.