lock down javascript and python permissions

This commit is contained in:
Robert Muir 2015-10-04 17:13:47 -04:00
parent e5a10e9520
commit 8ff42834e9
11 changed files with 279 additions and 32 deletions

View File

@ -45,9 +45,16 @@ public final class BootstrapInfo {
}
/**
* Returns true if secure computing mode is enabled (linux/amd64 only)
* Returns true if secure computing mode is enabled (linux/amd64, OS X only)
*/
public static boolean isSeccompInstalled() {
return Natives.isSeccompInstalled();
}
/**
* codebase location for untrusted scripts (provide some additional safety)
* <p>
* This is not a full URL, just a path.
*/
public static final String UNTRUSTED_CODEBASE = "/untrusted";
}

View File

@ -26,29 +26,27 @@ import java.net.URL;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.ProtectionDomain;
import java.security.URIParameter;
import java.util.PropertyPermission;
/** custom policy for union of static and dynamic permissions */
final class ESPolicy extends Policy {
/** template policy file, the one used in tests */
static final String POLICY_RESOURCE = "security.policy";
/** limited policy for groovy scripts */
static final String GROOVY_RESOURCE = "groovy.policy";
/** limited policy for scripts */
static final String UNTRUSTED_RESOURCE = "untrusted.policy";
final Policy template;
final Policy groovy;
final Policy untrusted;
final PermissionCollection dynamic;
public ESPolicy(PermissionCollection dynamic) throws Exception {
URI policyUri = getClass().getResource(POLICY_RESOURCE).toURI();
URI groovyUri = getClass().getResource(GROOVY_RESOURCE).toURI();
URI untrustedUri = getClass().getResource(UNTRUSTED_RESOURCE).toURI();
this.template = Policy.getInstance("JavaPolicy", new URIParameter(policyUri));
this.groovy = Policy.getInstance("JavaPolicy", new URIParameter(groovyUri));
this.untrusted = Policy.getInstance("JavaPolicy", new URIParameter(untrustedUri));
this.dynamic = dynamic;
}
@ -56,15 +54,17 @@ final class ESPolicy extends Policy {
public boolean implies(ProtectionDomain domain, Permission permission) {
CodeSource codeSource = domain.getCodeSource();
// codesource can be null when reducing privileges via doPrivileged()
if (codeSource != null) {
URL location = codeSource.getLocation();
// location can be null... ??? nobody knows
// https://bugs.openjdk.java.net/browse/JDK-8129972
if (location != null) {
// run groovy scripts with no permissions (except logging property)
if ("/groovy/script".equals(location.getFile())) {
return groovy.implies(domain, permission);
}
if (codeSource == null) {
return false;
}
URL location = codeSource.getLocation();
// location can be null... ??? nobody knows
// https://bugs.openjdk.java.net/browse/JDK-8129972
if (location != null) {
// run scripts with limited permissions
if (BootstrapInfo.UNTRUSTED_CODEBASE.equals(location.getFile())) {
return untrusted.implies(domain, permission);
}
}

View File

@ -69,8 +69,8 @@ grant codeBase "${es.security.plugin.lang-groovy}" {
permission java.lang.RuntimePermission "accessClassInPackage.sun.reflect";
// needed by GroovyScriptEngineService to close its classloader (why?)
permission java.lang.RuntimePermission "closeClassLoader";
// Allow executing groovy scripts with codesource of /groovy/script
permission groovy.security.GroovyCodeSourcePermission "/groovy/script";
// Allow executing groovy scripts with codesource of /untrusted
permission groovy.security.GroovyCodeSourcePermission "/untrusted";
};
grant codeBase "${es.security.plugin.lang-javascript}" {

View File

@ -18,8 +18,8 @@
*/
/*
* Limited security policy for groovy scripts.
* This is what is needed for its invokeDynamic functionality to work.
* Limited security policy for scripts.
* This is what is needed for invokeDynamic functionality to work.
*/
grant {

View File

@ -24,7 +24,9 @@ import org.elasticsearch.test.ESTestCase;
import java.io.FilePermission;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
@ -48,8 +50,13 @@ public class ESPolicyTests extends ESTestCase {
*/
public void testNullCodeSource() throws Exception {
assumeTrue("test cannot run with security manager", System.getSecurityManager() == null);
// create a policy with AllPermission
Permission all = new AllPermission();
PermissionCollection allCollection = all.newPermissionCollection();
allCollection.add(all);
ESPolicy policy = new ESPolicy(allCollection);
// restrict ourselves to NoPermission
PermissionCollection noPermissions = new Permissions();
ESPolicy policy = new ESPolicy(noPermissions);
assertFalse(policy.implies(new ProtectionDomain(null, noPermissions), new FilePermission("foo", "read")));
}

View File

@ -35,7 +35,6 @@ import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
@ -99,18 +98,24 @@ final class MockPluginPolicy extends Policy {
excludedSources.add(RandomizedRunner.class.getProtectionDomain().getCodeSource());
// junit library
excludedSources.add(Assert.class.getProtectionDomain().getCodeSource());
// groovy scripts
excludedSources.add(new CodeSource(new URL("file:/groovy/script"), (Certificate[])null));
// scripts
excludedSources.add(new CodeSource(new URL("file:" + BootstrapInfo.UNTRUSTED_CODEBASE), (Certificate[])null));
Loggers.getLogger(getClass()).debug("Apply permissions [{}] excluding codebases [{}]", extraPermissions, excludedSources);
}
@Override
public boolean implies(ProtectionDomain domain, Permission permission) {
CodeSource codeSource = domain.getCodeSource();
// codesource can be null when reducing privileges via doPrivileged()
if (codeSource == null) {
return false;
}
if (standardPolicy.implies(domain, permission)) {
return true;
} else if (excludedSources.contains(domain.getCodeSource()) == false &&
Objects.toString(domain.getCodeSource()).contains("test-classes") == false) {
} else if (excludedSources.contains(codeSource) == false &&
codeSource.toString().contains("test-classes") == false) {
return extraPermissions.implies(permission);
} else {
return false;

View File

@ -21,6 +21,7 @@ package org.elasticsearch.script.groovy;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.Script;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Scorer;
@ -36,6 +37,8 @@ import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.customizers.CompilationCustomizer;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.hash.MessageDigests;
@ -168,7 +171,15 @@ public class GroovyScriptEngineService extends AbstractComponent implements Scri
if (sm != null) {
sm.checkPermission(new SpecialPermission());
}
return loader.parseClass(script, MessageDigests.toHexString(MessageDigests.sha1().digest(script.getBytes(StandardCharsets.UTF_8))));
String fake = MessageDigests.toHexString(MessageDigests.sha1().digest(script.getBytes(StandardCharsets.UTF_8)));
// same logic as GroovyClassLoader.parseClass() but with a different codesource string:
GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
public GroovyCodeSource run() {
return new GroovyCodeSource(script, fake, BootstrapInfo.UNTRUSTED_CODEBASE);
}
});
gcs.setCachable(false);
return loader.parseClass(gcs);
} catch (Throwable e) {
if (logger.isTraceEnabled()) {
logger.trace("exception compiling Groovy script:", e);

View File

@ -22,6 +22,7 @@ package org.elasticsearch.script.javascript;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Scorer;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
@ -36,6 +37,10 @@ import org.mozilla.javascript.*;
import org.mozilla.javascript.Script;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
@ -105,7 +110,11 @@ public class JavaScriptScriptEngineService extends AbstractComponent implements
try {
ctx.setWrapFactory(wrapFactory);
ctx.setOptimizationLevel(optimizationLevel);
return ctx.compileString(script, generateScriptName(), 1, null);
ctx.setSecurityController(new PolicySecurityController());
return ctx.compileString(script, generateScriptName(), 1,
new CodeSource(new URL("file:" + BootstrapInfo.UNTRUSTED_CODEBASE), (Certificate[]) null));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
} finally {
Context.exit();
}

View File

@ -0,0 +1,89 @@
/*
* 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.javascript;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
import org.mozilla.javascript.WrappedException;
import java.util.HashMap;
import java.util.Map;
/**
* Tests for the Javascript security permissions
*/
public class JavaScriptSecurityTests extends ESTestCase {
private JavaScriptScriptEngineService se;
@Before
public void setup() {
se = new JavaScriptScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
}
@After
public void close() {
se.close();
}
/** runs a script */
private void doTest(String script) {
Map<String, Object> vars = new HashMap<String, Object>();
se.execute(new CompiledScript(ScriptService.ScriptType.INLINE, "test", "js", se.compile(script)), vars);
}
/** asserts that a script runs without exception */
private void assertSuccess(String script) {
doTest(script);
}
/** assert that a security exception is hit */
private void assertFailure(String script) {
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);
}
}
/** Test some javascripts that are ok */
public void testOK() {
assertSuccess("1 + 2");
assertSuccess("Math.cos(Math.PI)");
}
/** Test some javascripts that should hit security exception */
public void testNotOK() {
// sanity check :)
assertFailure("java.lang.Runtime.getRuntime().halt(0)");
// check a few things more restrictive than the ordinary policy
// no network
assertFailure("new java.net.Socket(\"localhost\", 1024)");
// no files
assertFailure("java.io.File.createTempFile(\"test\", \"tmp\")");
}
}

View File

@ -20,8 +20,11 @@
package org.elasticsearch.script.python;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.Map;
import org.apache.lucene.index.LeafReaderContext;
@ -125,7 +128,8 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
public Object execute(CompiledScript compiledScript, Map<String, Object> vars) {
PyObject pyVars = Py.java2py(vars);
interp.setLocals(pyVars);
PyObject ret = interp.eval((PyCode) compiledScript.compiled());
// eval the script with reduced privileges
PyObject ret = evalRestricted((PyCode) compiledScript.compiled());
if (ret == null) {
return null;
}
@ -171,7 +175,8 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
@Override
public Object run() {
interp.setLocals(pyVars);
PyObject ret = interp.eval(code);
// eval the script with reduced privileges
PyObject ret = evalRestricted(code);
if (ret == null) {
return null;
}
@ -229,7 +234,8 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
@Override
public Object run() {
interp.setLocals(pyVars);
PyObject ret = interp.eval(code);
// eval the script with reduced privileges
PyObject ret = evalRestricted(code);
if (ret == null) {
return null;
}
@ -257,6 +263,27 @@ public class PythonScriptEngineService extends AbstractComponent implements Scri
}
}
// we don't have a way to specify codesource for generated jython classes,
// so we just run them with a special context to reduce privileges
private static final AccessControlContext PY_CONTEXT;
static {
Permissions none = new Permissions();
none.setReadOnly();
PY_CONTEXT = new AccessControlContext(new ProtectionDomain[] {
new ProtectionDomain(null, none)
});
}
/** Evaluates with reduced privileges */
private final PyObject evalRestricted(final PyCode code) {
// eval the script with reduced privileges
return AccessController.doPrivileged(new PrivilegedAction<PyObject>() {
@Override
public PyObject run() {
return interp.eval(code);
}
}, PY_CONTEXT);
}
public static Object unwrapValue(Object value) {
if (value == null) {

View File

@ -0,0 +1,92 @@
/*
* 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.python;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.CompiledScript;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import org.junit.Before;
import org.python.core.PyException;
import java.util.HashMap;
import java.util.Map;
/**
* Tests for Python security permissions
*/
public class PythonSecurityTests extends ESTestCase {
private PythonScriptEngineService se;
@Before
public void setup() {
se = new PythonScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
}
@After
public void close() {
// We need to clear some system properties
System.clearProperty("python.cachedir.skip");
System.clearProperty("python.console.encoding");
se.close();
}
/** runs a script */
private void doTest(String script) {
Map<String, Object> vars = new HashMap<String, Object>();
se.execute(new CompiledScript(ScriptService.ScriptType.INLINE, "test", "python", se.compile(script)), vars);
}
/** asserts that a script runs without exception */
private void assertSuccess(String script) {
doTest(script);
}
/** assert that a security exception is hit */
private void assertFailure(String script) {
try {
doTest(script);
fail("did not get expected exception");
} catch (PyException expected) {
Throwable cause = expected.getCause();
assertNotNull("null cause for exception: " + expected, cause);
assertTrue("unexpected exception: " + cause, cause instanceof SecurityException);
}
}
/** Test some py scripts that are ok */
public void testOK() {
assertSuccess("1 + 2");
assertSuccess("from java.lang import Math\nMath.cos(0)");
}
/** Test some py scripts that should hit security exception */
public void testNotOK() {
// sanity check :)
assertFailure("from java.lang import Runtime\nRuntime.getRuntime().halt(0)");
// check a few things more restrictive than the ordinary policy
// no network
assertFailure("from java.net import Socket\nSocket(\"localhost\", 1024)");
// no files
assertFailure("from java.io import File\nFile.createTempFile(\"test\", \"tmp\")");
}
}