Filter classes loaded by scripts

Since 2.2 we run all scripts with minimal privileges, similar to applets in your browser.
The problem is, they have unrestricted access to other things they can muck with (ES, JDK, whatever).
So they can still easily do tons of bad things

This PR restricts what classes scripts can load via the classloader mechanism, to make life more difficult.
The "standard" list was populated from the old list used for the groovy sandbox: though
a few more were needed for tests to pass (java.lang.String, java.util.Iterator, nothing scary there).

Additionally, each scripting engine typically needs permissions to some runtime stuff.
That is the downside of this "good old classloader" approach, but I like the transparency and simplicity,
and I don't want to waste my time with any feature provided by the engine itself for this, I don't trust them.

This is not perfect and the engines are not perfect but you gotta start somewhere. For expert users that
need to tweak the permissions, we already support that via the standard java security configuration files, the
specification is simple, supports wildcards, etc (though we do not use them ourselves).
This commit is contained in:
Robert Muir 2015-12-05 21:46:52 -05:00
parent cea1c465d4
commit 2169a123a5
15 changed files with 479 additions and 69 deletions

View File

@ -0,0 +1,171 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.script;
import java.security.BasicPermission;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
/**
* Checked by scripting engines to allow loading a java class.
* <p>
* Examples:
* <p>
* Allow permission to {@code java.util.List}
* <pre>permission org.elasticsearch.script.ClassPermission "java.util.List";</pre>
* Allow permission to classes underneath {@code java.util} (and its subpackages such as {@code java.util.zip})
* <pre>permission org.elasticsearch.script.ClassPermission "java.util.*";</pre>
* Allow permission to standard predefined list of basic classes (see list below)
* <pre>permission org.elasticsearch.script.ClassPermission "&lt;&lt;STANDARD&gt;&gt;";</pre>
* Allow permission to all classes
* <pre>permission org.elasticsearch.script.ClassPermission "*";</pre>
* <p>
* Set of classes (allowed by special value <code>&lt;&lt;STANDARD&gt;&gt;</code>):
* <ul>
* <li>{@link java.lang.Boolean}</li>
* <li>{@link java.lang.Byte}</li>
* <li>{@link java.lang.Character}</li>
* <li>{@link java.lang.Double}</li>
* <li>{@link java.lang.Integer}</li>
* <li>{@link java.lang.Long}</li>
* <li>{@link java.lang.Math}</li>
* <li>{@link java.lang.Object}</li>
* <li>{@link java.lang.Short}</li>
* <li>{@link java.lang.String}</li>
* <li>{@link java.math.BigDecimal}</li>
* <li>{@link java.util.ArrayList}</li>
* <li>{@link java.util.Arrays}</li>
* <li>{@link java.util.Date}</li>
* <li>{@link java.util.HashMap}</li>
* <li>{@link java.util.HashSet}</li>
* <li>{@link java.util.Iterator}</li>
* <li>{@link java.util.List}</li>
* <li>{@link java.util.Map}</li>
* <li>{@link java.util.Set}</li>
* <li>{@link java.util.UUID}</li>
* <li>{@link org.joda.time.DateTime}</li>
* <li>{@link org.joda.time.DateTimeUtils}</li>
* <li>{@link org.joda.time.DateTimeZone}</li>
* <li>{@link org.joda.time.Instant}</li>
* </ul>
*/
public final class ClassPermission extends BasicPermission {
private static final long serialVersionUID = 3530711429252193884L;
public static final String STANDARD = "<<STANDARD>>";
/** Typical set of classes for scripting: basic data types, math, dates, and simple collections */
// this is the list from the old grovy sandbox impl (+ some things like String, Iterator, etc that were missing)
public static final Set<String> STANDARD_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
// jdk classes
java.lang.Boolean.class.getName(),
java.lang.Byte.class.getName(),
java.lang.Character.class.getName(),
java.lang.Double.class.getName(),
java.lang.Integer.class.getName(),
java.lang.Long.class.getName(),
java.lang.Math.class.getName(),
java.lang.Object.class.getName(),
java.lang.Short.class.getName(),
java.lang.String.class.getName(),
java.math.BigDecimal.class.getName(),
java.util.ArrayList.class.getName(),
java.util.Arrays.class.getName(),
java.util.Date.class.getName(),
java.util.HashMap.class.getName(),
java.util.HashSet.class.getName(),
java.util.Iterator.class.getName(),
java.util.List.class.getName(),
java.util.Map.class.getName(),
java.util.Set.class.getName(),
java.util.UUID.class.getName(),
// joda-time
org.joda.time.DateTime.class.getName(),
org.joda.time.DateTimeUtils.class.getName(),
org.joda.time.DateTimeZone.class.getName(),
org.joda.time.Instant.class.getName()
)));
/**
* Creates a new ClassPermission object.
*
* @param name class to grant permission to
*/
public ClassPermission(String name) {
super(name);
}
/**
* Creates a new ClassPermission object.
* This constructor exists for use by the {@code Policy} object to instantiate new Permission objects.
*
* @param name class to grant permission to
* @param actions ignored
*/
public ClassPermission(String name, String actions) {
this(name);
}
@Override
public boolean implies(Permission p) {
// check for a special value of STANDARD to imply the basic set
if (p != null && p.getClass() == getClass()) {
ClassPermission other = (ClassPermission) p;
if (STANDARD.equals(getName()) && STANDARD_CLASSES.contains(other.getName())) {
return true;
}
}
return super.implies(p);
}
@Override
public PermissionCollection newPermissionCollection() {
// BasicPermissionCollection only handles wildcards, we expand <<STANDARD>> here
PermissionCollection impl = super.newPermissionCollection();
return new PermissionCollection() {
private static final long serialVersionUID = 6792220143549780002L;
@Override
public void add(Permission permission) {
if (permission instanceof ClassPermission && STANDARD.equals(permission.getName())) {
for (String clazz : STANDARD_CLASSES) {
impl.add(new ClassPermission(clazz));
}
} else {
impl.add(permission);
}
}
@Override
public boolean implies(Permission permission) {
return impl.implies(permission);
}
@Override
public Enumeration<Permission> elements() {
return impl.elements();
}
};
}
}

View File

@ -34,5 +34,6 @@ grant {
permission java.util.PropertyPermission "rhino.stack.style", "read";
// needed IndyInterface selectMethod (setCallSiteTarget)
// TODO: clean this up / only give it to engines that really must have it
permission java.lang.RuntimePermission "getClassLoader";
};

View File

@ -0,0 +1,79 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.script;
import org.elasticsearch.test.ESTestCase;
import java.security.AllPermission;
import java.security.PermissionCollection;
/** Very simple sanity checks for {@link ClassPermission} */
public class ClassPermissionTests extends ESTestCase {
public void testEquals() {
assertEquals(new ClassPermission("pkg.MyClass"), new ClassPermission("pkg.MyClass"));
assertFalse(new ClassPermission("pkg.MyClass").equals(new AllPermission()));
}
public void testImplies() {
assertTrue(new ClassPermission("pkg.MyClass").implies(new ClassPermission("pkg.MyClass")));
assertFalse(new ClassPermission("pkg.MyClass").implies(new ClassPermission("pkg.MyOtherClass")));
assertFalse(new ClassPermission("pkg.MyClass").implies(null));
assertFalse(new ClassPermission("pkg.MyClass").implies(new AllPermission()));
}
public void testStandard() {
assertTrue(new ClassPermission("<<STANDARD>>").implies(new ClassPermission("java.lang.Math")));
assertFalse(new ClassPermission("<<STANDARD>>").implies(new ClassPermission("pkg.MyClass")));
}
public void testPermissionCollection() {
ClassPermission math = new ClassPermission("java.lang.Math");
PermissionCollection collection = math.newPermissionCollection();
collection.add(math);
assertTrue(collection.implies(new ClassPermission("java.lang.Math")));
assertFalse(collection.implies(new ClassPermission("pkg.MyClass")));
}
public void testPermissionCollectionStandard() {
ClassPermission standard = new ClassPermission("<<STANDARD>>");
PermissionCollection collection = standard.newPermissionCollection();
collection.add(standard);
assertTrue(collection.implies(new ClassPermission("java.lang.Math")));
assertFalse(collection.implies(new ClassPermission("pkg.MyClass")));
}
/** not recommended but we test anyway */
public void testWildcards() {
assertTrue(new ClassPermission("*").implies(new ClassPermission("pkg.MyClass")));
assertTrue(new ClassPermission("pkg.*").implies(new ClassPermission("pkg.MyClass")));
assertTrue(new ClassPermission("pkg.*").implies(new ClassPermission("pkg.sub.MyClass")));
assertFalse(new ClassPermission("pkg.My*").implies(new ClassPermission("pkg.MyClass")));
assertFalse(new ClassPermission("pkg*").implies(new ClassPermission("pkg.MyClass")));
}
public void testPermissionCollectionWildcards() {
ClassPermission lang = new ClassPermission("java.lang.*");
PermissionCollection collection = lang.newPermissionCollection();
collection.add(lang);
assertTrue(collection.implies(new ClassPermission("java.lang.Math")));
assertFalse(collection.implies(new ClassPermission("pkg.MyClass")));
}
}

View File

@ -36,6 +36,7 @@ import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.index.mapper.core.DateFieldMapper;
import org.elasticsearch.index.mapper.core.NumberFieldMapper;
import org.elasticsearch.script.ClassPermission;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptEngineService;
@ -95,7 +96,7 @@ public class ExpressionScriptEngineService extends AbstractComponent implements
@Override
public Object compile(String script) {
// classloader created here
SecurityManager sm = System.getSecurityManager();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
@ -103,8 +104,22 @@ public class ExpressionScriptEngineService extends AbstractComponent implements
@Override
public Expression run() {
try {
ClassLoader loader = getClass().getClassLoader();
if (sm != null) {
loader = new ClassLoader(loader) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
sm.checkPermission(new ClassPermission(name));
} catch (SecurityException e) {
throw new ClassNotFoundException(name, e);
}
return super.loadClass(name, resolve);
}
};
}
// NOTE: validation is delayed to allow runtime vars, and we don't have access to per index stuff here
return JavascriptCompiler.compile(script);
return JavascriptCompiler.compile(script, JavascriptCompiler.DEFAULT_FUNCTIONS, loader);
} catch (ParseException e) {
throw new ScriptException("Failed to parse expression: " + script, e);
}

View File

@ -22,4 +22,13 @@ grant {
permission java.lang.RuntimePermission "createClassLoader";
// needed because of security problems in JavascriptCompiler
permission java.lang.RuntimePermission "getClassLoader";
// expression runtime
permission org.elasticsearch.script.ClassPermission "java.lang.String";
permission org.elasticsearch.script.ClassPermission "org.apache.lucene.expressions.Expression";
permission org.elasticsearch.script.ClassPermission "org.apache.lucene.queries.function.FunctionValues";
// available functions
permission org.elasticsearch.script.ClassPermission "java.lang.Math";
permission org.elasticsearch.script.ClassPermission "org.apache.lucene.util.MathUtil";
permission org.elasticsearch.script.ClassPermission "org.apache.lucene.util.SloppyMath";
};

View File

@ -98,6 +98,16 @@ public class MoreExpressionTests extends ESIntegTestCase {
assertEquals(5.0, rsp.getHits().getAt(0).field("foo").getValue(), 0.0D);
}
public void testFunction() throws Exception {
createIndex("test");
ensureGreen("test");
client().prepareIndex("test", "doc", "1").setSource("foo", 4).setRefresh(true).get();
SearchResponse rsp = buildRequest("doc['foo'] + abs(1)").get();
assertSearchResponse(rsp);
assertEquals(1, rsp.getHits().getTotalHits());
assertEquals(5.0, rsp.getHits().getAt(0).field("foo").getValue(), 0.0D);
}
public void testBasicUsingDotValue() throws Exception {
createIndex("test");
ensureGreen("test");

View File

@ -51,6 +51,7 @@ import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
@ -65,16 +66,6 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
* The name of the scripting engine/language.
*/
public static final String NAME = "groovy";
/**
* The setting to enable or disable <code>invokedynamic</code> instruction support in Java 7+.
* <p>
* Note: If this is disabled because <code>invokedynamic</code> is causing issues, then the Groovy
* <code>indy</code> jar needs to be replaced by the non-<code>indy</code> variant of it on the classpath (e.g.,
* <code>groovy-all-2.4.4-indy.jar</code> should be replaced by <code>groovy-all-2.4.4.jar</code>).
* <p>
* Defaults to {@code true}.
*/
public static final String GROOVY_INDY_ENABLED = "script.groovy.indy";
/**
* The name of the Groovy compiler setting to use associated with activating <code>invokedynamic</code> support.
*/
@ -96,22 +87,33 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
// Add BigDecimal -> Double transformer
config.addCompilationCustomizers(new GroovyBigDecimalTransformer(CompilePhase.CONVERSION));
// Implicitly requires Java 7u60 or later to get valid support
if (settings.getAsBoolean(GROOVY_INDY_ENABLED, true)) {
// maintain any default optimizations
config.getOptimizationOptions().put(GROOVY_INDY_SETTING_NAME, true);
}
// always enable invokeDynamic, not the crazy softreference-based stuff
config.getOptimizationOptions().put(GROOVY_INDY_SETTING_NAME, true);
// Groovy class loader to isolate Groovy-land code
// classloader created here
SecurityManager sm = System.getSecurityManager();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
this.loader = AccessController.doPrivileged(new PrivilegedAction<GroovyClassLoader>() {
@Override
public GroovyClassLoader run() {
return new GroovyClassLoader(getClass().getClassLoader(), config);
// snapshot our context (which has permissions for classes), since the script has none
final AccessControlContext engineContext = AccessController.getContext();
return new GroovyClassLoader(new ClassLoader(getClass().getClassLoader()) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (sm != null) {
try {
engineContext.checkPermission(new ClassPermission(name));
} catch (SecurityException e) {
throw new ClassNotFoundException(name, e);
}
}
return super.loadClass(name, resolve);
}
}, config);
}
});
}

View File

@ -28,4 +28,24 @@ grant {
permission java.lang.RuntimePermission "closeClassLoader";
// Allow executing groovy scripts with codesource of /untrusted
permission groovy.security.GroovyCodeSourcePermission "/untrusted";
// Standard set of classes
permission org.elasticsearch.script.ClassPermission "<<STANDARD>>";
// groovy runtime (TODO: clean these up if possible)
permission org.elasticsearch.script.ClassPermission "groovy.grape.GrabAnnotationTransformation";
permission org.elasticsearch.script.ClassPermission "groovy.json.JsonOutput";
permission org.elasticsearch.script.ClassPermission "groovy.lang.Binding";
permission org.elasticsearch.script.ClassPermission "groovy.lang.GroovyObject";
permission org.elasticsearch.script.ClassPermission "groovy.lang.GString";
permission org.elasticsearch.script.ClassPermission "groovy.lang.Script";
permission org.elasticsearch.script.ClassPermission "groovy.util.GroovyCollections";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.ast.builder.AstBuilderTransformation";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.reflection.ClassInfo";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.GStringImpl";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.powerassert.ValueRecorder";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.powerassert.AssertionRenderer";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.ScriptBytecodeAdapter";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation";
permission org.elasticsearch.script.ClassPermission "org.codehaus.groovy.vmplugin.v7.IndyInterface";
permission org.elasticsearch.script.ClassPermission "sun.reflect.ConstructorAccessorImpl";
};

View File

@ -20,16 +20,22 @@
package org.elasticsearch.script.groovy;
import org.apache.lucene.util.Constants;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import groovy.lang.MissingPropertyException;
import java.nio.file.Path;
import java.security.PrivilegedActionException;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -48,7 +54,7 @@ public class GroovySecurityTests extends ESTestCase {
@Override
public void setUp() throws Exception {
super.setUp();
se = new GroovyScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
se = new GroovyScriptEngineService(Settings.EMPTY);
// otherwise will exit your VM and other bad stuff
assumeTrue("test requires security manager to be enabled", System.getSecurityManager() != null);
}
@ -62,8 +68,16 @@ public class GroovySecurityTests extends ESTestCase {
public void testEvilGroovyScripts() throws Exception {
// Plain test
assertSuccess("");
// field access
// field access (via map)
assertSuccess("def foo = doc['foo'].value; if (foo == null) { return 5; }");
// field access (via list)
assertSuccess("def foo = mylist[0]; if (foo == null) { return 5; }");
// field access (via array)
assertSuccess("def foo = myarray[0]; if (foo == null) { return 5; }");
// field access (via object)
assertSuccess("def foo = myobject.primitive.toString(); if (foo == null) { return 5; }");
assertSuccess("def foo = myobject.object.toString(); if (foo == null) { return 5; }");
assertSuccess("def foo = myobject.list[0].primitive.toString(); if (foo == null) { return 5; }");
// List
assertSuccess("def list = [doc['foo'].value, 3, 4]; def v = list.get(1); list.add(10)");
// Ranges
@ -78,35 +92,35 @@ public class GroovySecurityTests extends ESTestCase {
assertSuccess("def n = [1,2,3]; GroovyCollections.max(n)");
// Fail cases:
// AccessControlException[access denied ("java.io.FilePermission" "<<ALL FILES>>" "execute")]
assertFailure("pr = Runtime.getRuntime().exec(\"touch /tmp/gotcha\"); pr.waitFor()");
assertFailure("pr = Runtime.getRuntime().exec(\"touch /tmp/gotcha\"); pr.waitFor()", MissingPropertyException.class);
// AccessControlException[access denied ("java.lang.RuntimePermission" "accessClassInPackage.sun.reflect")]
assertFailure("d = new DateTime(); d.getClass().getDeclaredMethod(\"year\").setAccessible(true)");
// infamous:
assertFailure("java.lang.Math.class.forName(\"java.lang.Runtime\")", PrivilegedActionException.class);
// filtered directly by our classloader
assertFailure("getClass().getClassLoader().loadClass(\"java.lang.Runtime\").availableProcessors()", PrivilegedActionException.class);
// unfortunately, we have access to other classloaders (due to indy mechanism needing getClassLoader permission)
// but we can't do much with them directly at least.
assertFailure("myobject.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").availableProcessors()", SecurityException.class);
assertFailure("d = new DateTime(); d.getClass().getDeclaredMethod(\"year\").setAccessible(true)", SecurityException.class);
assertFailure("d = new DateTime(); d.\"${'get' + 'Class'}\"()." +
"\"${'getDeclared' + 'Method'}\"(\"year\").\"${'set' + 'Accessible'}\"(false)");
assertFailure("Class.forName(\"org.joda.time.DateTime\").getDeclaredMethod(\"year\").setAccessible(true)");
"\"${'getDeclared' + 'Method'}\"(\"year\").\"${'set' + 'Accessible'}\"(false)", SecurityException.class);
assertFailure("Class.forName(\"org.joda.time.DateTime\").getDeclaredMethod(\"year\").setAccessible(true)", MissingPropertyException.class);
// AccessControlException[access denied ("groovy.security.GroovyCodeSourcePermission" "/groovy/shell")]
assertFailure("Eval.me('2 + 2')");
assertFailure("Eval.x(5, 'x + 2')");
assertFailure("Eval.me('2 + 2')", MissingPropertyException.class);
assertFailure("Eval.x(5, 'x + 2')", MissingPropertyException.class);
// AccessControlException[access denied ("java.lang.RuntimePermission" "accessDeclaredMembers")]
assertFailure("d = new Date(); java.lang.reflect.Field f = Date.class.getDeclaredField(\"fastTime\");" +
" f.setAccessible(true); f.get(\"fastTime\")");
" f.setAccessible(true); f.get(\"fastTime\")", MultipleCompilationErrorsException.class);
// AccessControlException[access denied ("java.io.FilePermission" "<<ALL FILES>>" "execute")]
assertFailure("def methodName = 'ex'; Runtime.\"${'get' + 'Runtime'}\"().\"${methodName}ec\"(\"touch /tmp/gotcha2\")");
assertFailure("def methodName = 'ex'; Runtime.\"${'get' + 'Runtime'}\"().\"${methodName}ec\"(\"touch /tmp/gotcha2\")", MissingPropertyException.class);
// AccessControlException[access denied ("java.lang.RuntimePermission" "modifyThreadGroup")]
assertFailure("t = new Thread({ println 3 });");
assertFailure("t = new Thread({ println 3 });", MultipleCompilationErrorsException.class);
// test a directory we normally have access to, but the groovy script does not.
Path dir = createTempDir();
// TODO: figure out the necessary escaping for windows paths here :)
if (!Constants.WINDOWS) {
// access denied ("java.io.FilePermission" ".../tempDir-00N" "read")
assertFailure("new File(\"" + dir + "\").exists()");
assertFailure("new File(\"" + dir + "\").exists()", MultipleCompilationErrorsException.class);
}
}
@ -115,23 +129,35 @@ public class GroovySecurityTests extends ESTestCase {
Map<String, Object> vars = new HashMap<String, Object>();
// we add a "mock document" containing a single field "foo" that returns 4 (abusing a jdk class with a getValue() method)
vars.put("doc", Collections.singletonMap("foo", new AbstractMap.SimpleEntry<Object,Integer>(null, 4)));
vars.put("mylist", Arrays.asList("foo"));
vars.put("myarray", Arrays.asList("foo"));
vars.put("myobject", new MyObject());
se.executable(new CompiledScript(ScriptService.ScriptType.INLINE, "test", "js", se.compile(script)), vars).run();
}
public static class MyObject {
public int getPrimitive() { return 0; }
public Object getObject() { return "value"; }
public List<Object> getList() { return Arrays.asList(new MyObject()); }
}
/** asserts that a script runs without exception */
private void assertSuccess(String script) {
doTest(script);
}
/** asserts that a script triggers securityexception */
private void assertFailure(String script) {
private void assertFailure(String script, Class<? extends Throwable> exceptionClass) {
try {
doTest(script);
fail("did not get expected exception");
} catch (ScriptException expected) {
Throwable cause = expected.getCause();
assertNotNull(cause);
assertTrue("unexpected exception: " + cause, cause instanceof SecurityException);
if (exceptionClass.isAssignableFrom(cause.getClass()) == false) {
throw new AssertionError("unexpected exception: " + cause, expected);
}
}
}
}

View File

@ -39,7 +39,10 @@ import org.mozilla.javascript.Script;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
@ -54,18 +57,47 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
private static WrapFactory wrapFactory = new CustomWrapFactory();
private final int optimizationLevel;
private Scriptable globalScope;
// one time initialization of rhino security manager integration
private static final CodeSource DOMAIN;
private static final int OPTIMIZATION_LEVEL = 1;
static {
try {
DOMAIN = new CodeSource(new URL("file:" + BootstrapInfo.UNTRUSTED_CODEBASE), (Certificate[]) null);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
ContextFactory factory = new ContextFactory() {
@Override
protected void onContextCreated(Context cx) {
cx.setWrapFactory(wrapFactory);
cx.setOptimizationLevel(OPTIMIZATION_LEVEL);
}
};
if (System.getSecurityManager() != null) {
factory.initApplicationClassLoader(AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
// snapshot our context (which has permissions for classes), since the script has none
final AccessControlContext engineContext = AccessController.getContext();
return new ClassLoader(JavaScriptScriptEngineService.class.getClassLoader()) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
engineContext.checkPermission(new ClassPermission(name));
} catch (SecurityException e) {
throw new ClassNotFoundException(name, e);
}
return super.loadClass(name, resolve);
}
};
}
}));
}
factory.seal();
ContextFactory.initGlobal(factory);
SecurityController.initGlobal(new PolicySecurityController() {
@Override
public GeneratedClassLoader createClassLoader(ClassLoader parent, Object securityDomain) {
@ -78,6 +110,7 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
if (securityDomain != DOMAIN) {
throw new SecurityException("illegal securityDomain: " + securityDomain);
}
return super.createClassLoader(parent, securityDomain);
}
});
@ -90,11 +123,8 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public JavaScriptScriptEngineService(Settings settings) {
super(settings);
this.optimizationLevel = settings.getAsInt("script.javascript.optimization_level", 1);
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
globalScope = ctx.initStandardObjects(null, true);
} finally {
Context.exit();
@ -130,8 +160,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public Object compile(String script) {
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
ctx.setOptimizationLevel(optimizationLevel);
return ctx.compileString(script, generateScriptName(), 1, DOMAIN);
} finally {
Context.exit();
@ -142,8 +170,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public ExecutableScript executable(CompiledScript compiledScript, Map<String, Object> vars) {
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
Scriptable scope = ctx.newObject(globalScope);
scope.setPrototype(globalScope);
scope.setParentScope(null);
@ -161,8 +187,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public SearchScript search(final CompiledScript compiledScript, final SearchLookup lookup, @Nullable final Map<String, Object> vars) {
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
final Scriptable scope = ctx.newObject(globalScope);
scope.setPrototype(globalScope);
scope.setParentScope(null);
@ -215,7 +239,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public Object run() {
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
return ScriptValueConverter.unwrapValue(script.exec(ctx, scope));
} finally {
Context.exit();
@ -276,7 +299,6 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
public Object run() {
Context ctx = Context.enter();
try {
ctx.setWrapFactory(wrapFactory);
return ScriptValueConverter.unwrapValue(script.exec(ctx, scope));
} finally {
Context.exit();

View File

@ -20,4 +20,15 @@
grant {
// needed to generate runtime classes
permission java.lang.RuntimePermission "createClassLoader";
// Standard set of classes
permission org.elasticsearch.script.ClassPermission "<<STANDARD>>";
// rhino runtime (TODO: clean these up if possible)
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.ContextFactory";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.Callable";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.NativeFunction";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.Script";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.ScriptRuntime";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.Undefined";
permission org.elasticsearch.script.ClassPermission "org.mozilla.javascript.optimizer.OptRuntime";
};

View File

@ -23,6 +23,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.mozilla.javascript.EcmaError;
import org.mozilla.javascript.WrappedException;
import java.util.HashMap;
@ -61,14 +62,20 @@ public class JavaScriptSecurityTests extends ESTestCase {
}
/** assert that a security exception is hit */
private void assertFailure(String script) {
private void assertFailure(String script, Class<? extends Throwable> exceptionClass) {
try {
doTest(script);
fail("did not get expected exception");
} catch (WrappedException expected) {
Throwable cause = expected.getCause();
assertNotNull(cause);
assertTrue("unexpected exception: " + cause, cause instanceof SecurityException);
if (exceptionClass.isAssignableFrom(cause.getClass()) == false) {
throw new AssertionError("unexpected exception: " + expected, expected);
}
} catch (EcmaError expected) {
if (exceptionClass.isAssignableFrom(expected.getClass()) == false) {
throw new AssertionError("unexpected exception: " + expected, expected);
}
}
}
@ -79,22 +86,22 @@ public class JavaScriptSecurityTests extends ESTestCase {
}
/** Test some javascripts that should hit security exception */
public void testNotOK() {
public void testNotOK() throws Exception {
// sanity check :)
assertFailure("java.lang.Runtime.getRuntime().halt(0)");
assertFailure("java.lang.Runtime.getRuntime().halt(0)", EcmaError.class);
// check a few things more restrictive than the ordinary policy
// no network
assertFailure("new java.net.Socket(\"localhost\", 1024)");
assertFailure("new java.net.Socket(\"localhost\", 1024)", EcmaError.class);
// no files
assertFailure("java.io.File.createTempFile(\"test\", \"tmp\")");
assertFailure("java.io.File.createTempFile(\"test\", \"tmp\")", EcmaError.class);
}
public void testDefinitelyNotOK() {
// no mucking with security controller
assertFailure("var ctx = org.mozilla.javascript.Context.getCurrentContext(); " +
"ctx.setSecurityController(new org.mozilla.javascript.PolicySecurityController());");
"ctx.setSecurityController(new org.mozilla.javascript.PolicySecurityController());", EcmaError.class);
// no compiling scripts from scripts
assertFailure("var ctx = org.mozilla.javascript.Context.getCurrentContext(); " +
"ctx.compileString(\"1 + 1\", \"foobar\", 1, null); ");
"ctx.compileString(\"1 + 1\", \"foobar\", 1, null); ", EcmaError.class);
}
}

View File

@ -25,7 +25,11 @@ import java.security.AccessController;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Scorer;
@ -34,6 +38,7 @@ import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.ClassPermission;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript;
@ -61,14 +66,30 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
super(settings);
// classloader created here
SecurityManager sm = System.getSecurityManager();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
this.interp = AccessController.doPrivileged(new PrivilegedAction<PythonInterpreter> () {
@Override
public PythonInterpreter run() {
return PythonInterpreter.threadLocalStateInterpreter(null);
// snapshot our context here for checks, as the script has no permissions
final AccessControlContext engineContext = AccessController.getContext();
PythonInterpreter interp = PythonInterpreter.threadLocalStateInterpreter(null);
if (sm != null) {
interp.getSystemState().setClassLoader(new ClassLoader(getClass().getClassLoader()) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
try {
engineContext.checkPermission(new ClassPermission(name));
} catch (SecurityException e) {
throw new ClassNotFoundException(name, e);
}
return super.loadClass(name, resolve);
}
});
}
return interp;
}
});
}

View File

@ -22,4 +22,6 @@ grant {
permission java.lang.RuntimePermission "createClassLoader";
// needed by PySystemState init (TODO: see if we can avoid this)
permission java.lang.RuntimePermission "getClassLoader";
// Standard set of classes
permission org.elasticsearch.script.ClassPermission "<<STANDARD>>";
};

View File

@ -25,7 +25,9 @@ import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.python.core.PyException;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
@ -66,12 +68,12 @@ public class PythonSecurityTests extends ESTestCase {
doTest(script);
fail("did not get expected exception");
} catch (PyException expected) {
Throwable cause = expected.getCause();
// TODO: fix jython localization bugs: https://github.com/elastic/elasticsearch/issues/13967
// this is the correct assert:
// assertNotNull("null cause for exception: " + expected, cause);
assertNotNull("null cause for exception", cause);
assertTrue("unexpected exception: " + cause, cause instanceof SecurityException);
// we do a gross hack for now
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.getDefault());
if (symbols.getZeroDigit() == '0') {
assertTrue(expected.toString().contains("cannot import"));
}
}
}
@ -91,4 +93,16 @@ public class PythonSecurityTests extends ESTestCase {
// no files
assertFailure("from java.io import File\nFile.createTempFile(\"test\", \"tmp\")");
}
/** Test again from a new thread, python has complex threadlocal configuration */
public void testNotOKFromSeparateThread() throws Exception {
Thread t = new Thread() {
@Override
public void run() {
assertFailure("from java.lang import Runtime\nRuntime.availableProcessors()");
}
};
t.start();
t.join();
}
}