Merge pull request #15262 from rmuir/filter_classes_in_scripts
Filter classes loaded by scripts
This commit is contained in:
commit
1329ef487a
|
@ -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 "<<STANDARD>>";</pre>
|
||||
* Allow permission to all classes
|
||||
* <pre>permission org.elasticsearch.script.ClassPermission "*";</pre>
|
||||
* <p>
|
||||
* Set of classes (allowed by special value <code><<STANDARD>></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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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")));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -44,6 +45,7 @@ import org.elasticsearch.script.SearchScript;
|
|||
import org.elasticsearch.search.MultiValueMode;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
|
||||
import java.security.AccessControlContext;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.text.ParseException;
|
||||
|
@ -95,7 +97,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 +105,24 @@ public class ExpressionScriptEngineService extends AbstractComponent implements
|
|||
@Override
|
||||
public Expression run() {
|
||||
try {
|
||||
// snapshot our context here, we check on behalf of the expression
|
||||
AccessControlContext engineContext = AccessController.getContext();
|
||||
ClassLoader loader = getClass().getClassLoader();
|
||||
if (sm != null) {
|
||||
loader = new ClassLoader(loader) {
|
||||
@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);
|
||||
}
|
||||
};
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -97,6 +97,16 @@ public class MoreExpressionTests extends ESIntegTestCase {
|
|||
assertEquals(1, rsp.getHits().getTotalHits());
|
||||
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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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,8 +129,18 @@ 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) {
|
||||
|
@ -124,14 +148,16 @@ public class GroovySecurityTests extends ESTestCase {
|
|||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -55,20 +60,36 @@ import org.python.util.PythonInterpreter;
|
|||
public class PythonScriptEngineService extends AbstractComponent implements ScriptEngineService {
|
||||
|
||||
private final PythonInterpreter interp;
|
||||
|
||||
|
||||
@Inject
|
||||
public PythonScriptEngineService(Settings settings) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>>";
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue