/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.security;
import java.awt.AWTPermission;
import java.io.FilePermission;
import java.lang.reflect.ReflectPermission;
import java.net.SocketPermission;
import java.net.URLPermission;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.spec.X509EncodedKeySpec;
import java.util.PropertyPermission;
import java.util.logging.Level;
import java.util.logging.LoggingPermission;
import javax.sound.sampled.AudioPermission;
import javax.xml.bind.DatatypeConverter;
import com.rapidminer.RapidMiner;
import com.rapidminer.core.license.ProductConstraintManager;
import com.rapidminer.license.StudioLicenseConstants;
import com.rapidminer.operator.ScriptingOperator;
import com.rapidminer.security.internal.InternalPluginClassLoader;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.ParameterService;
import com.rapidminer.tools.plugin.Plugin;
import com.rapidminer.tools.plugin.PluginClassLoader;
/**
* This class is responsible for restricting access to certain capabilities of Java for
* {@link Plugin}s. Only extensions that are signed by us and would pass the JarVerifier check are
* granted trusted access, i.e. they will get the same permissions as our core code. Untrusted
* extensions will get a limited set of permissions.
*
* @author Marco Boeck
* @since 7.2
*/
public final class PluginSandboxPolicy extends Policy {
/** Internal permission for {@link RuntimePermission}s */
public static final String RAPIDMINER_INTERNAL_PERMISSION = "accessClassInPackage.rapidminer.internal";
/** The key pair algorithm for our signed extensions */
private static final String KEY_ALGORITHM = "RSA";
/** The Base64 encoded public key which is used to verify our signed extensions */
private static final String KEY_B64_ENCODED = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyoyYgZ0jYHlPOh2mGvvvXl6FS4X"
+ "t3FaCsnn1IglbbDYM9eXcWgeD6I/4mM3t6XsAsyzSDLRxagCM869lYknxjff0xMdA5aekqPe0vx4yqR9QK369u3lbGMaNvylwhg5vCTWn2vZ"
+ "anxWScOfVW6yDxEjgEHJvMiMzZkGNklYC3ULBCkHfIrih5hO83k5FileuUWDNO4BrLrawmjo9AmYksPVOMmd4/DtDpnehpLy0hQtjBJsz61h"
+ "AGVDnPGpvbsW0rjFAjE4fR5+4RwUNo+SsD/44Jc8bui5seVH5vZuTj02XokybGR4BikrqvJZ4rHe4OGowl8uIr9sEN/+0eIJXQIDAQAB";
/**
* the system property which can be set to {@code true} to enforce plugin sandboxing even on
* SNAPSHOT versions
*/
private static final String PROPERTY_SECURITY_ENFORCED = "com.rapidminer.security.enforce";
/** Our public key used to verify the certificates */
private static PublicKey key;
/**
* if {@code true}, plugin sandboxing is enforced even on SNAPSHOT versions
*/
private static volatile Boolean enforced;
static {
try {
KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(DatatypeConverter.parseBase64Binary(KEY_B64_ENCODED));
key = factory.generatePublic(spec);
} catch (GeneralSecurityException e) {
key = null;
// no log service available yet, so use syserr
System.err.println(
"Failed to initialize public key to verify extension certificates. Revoking permissions for all extensions!");
e.printStackTrace();
}
}
@Override
public PermissionCollection getPermissions(ProtectionDomain domain) {
if (isInternalPlugin(domain)) {
// used e.g. by Radoop for external library loading
return createAllPermissions();
} else if (isUnsignedPlugin(domain)) {
return createUnsignedPermissions((PluginClassLoader) domain.getClassLoader());
} else if (isUnknown(domain)) {
return createUnknownSourcePermissions();
} else if (isGroovyScript(domain)) {
return createGroovySourcePermissions();
} else {
return createAllPermissions();
}
}
@Override
public PermissionCollection getPermissions(CodeSource codesource) {
// This is a workaround for the following bug
// https://bugs.openjdk.java.net/browse/JDK-8014008
// return modifiable empty permissions, to avoid manipulation of read only permissions
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
if ("sun.rmi.server.LoaderHandler".equals(element.getClassName())
&& ("loadClass".equals(element.getMethodName()) || "loadProxyClass".equals(element.getMethodName()))) {
return new Permissions();
}
}
// return unmodifiable Policy.UNSUPPORTED_EMPTY_COLLECTION
return super.getPermissions(codesource);
}
/**
* Checks whether the given domain belongs to a special internal extension or not.
*
* @param domain
* the domain in question, never {@code null}
* @return {@code true} if the domain belongs to a special internal extension; {@code false}
* otherwise
*
*/
private static boolean isInternalPlugin(ProtectionDomain domain) {
// everything not loaded by the internal plugin classloader is not a special internal plugin
return domain.getClassLoader() instanceof InternalPluginClassLoader;
}
/**
* Checks whether the given domain belongs to an unsigned extension or not.
*
* @param domain
* the domain in question, never {@code null}
* @return {@code true} if the domain belongs to an unsigned extension; {@code false} otherwise
*
*/
private static boolean isUnsignedPlugin(ProtectionDomain domain) {
// everything not loaded by the plugin classloader is no plugin
if (!(domain.getClassLoader() instanceof PluginClassLoader)) {
return false;
}
// if the public key could not be initialized, we treat all plugins as unsafe
if (key == null) {
return true;
}
// some sanity checks
if (domain.getCodeSource() == null) {
return true;
}
// special case for SNAPSHOT version: grant all permissions for all extensions
// unless security is enforced via system property
if (RapidMiner.getVersion().isSnapshot() && !isSecurityEnforced()) {
return false;
}
if (domain.getCodeSource().getCertificates() == null) {
// if no certificate: unsigned permissions only
return true;
}
try {
verifyCertificates(domain.getCodeSource().getCertificates());
// signed by us, we are good at this point as we found a valid certificate
return false;
} catch (GeneralSecurityException e) {
// invalid certificate
LogService.getRoot().log(Level.WARNING, "Invalid certificate for " + domain.getCodeSource().getLocation());
return true;
} catch (Exception e) {
// some other error during certificate verification
LogService.getRoot().log(Level.WARNING,
"Error verifying certificate for " + domain.getCodeSource().getLocation(), e);
return true;
}
}
/**
* Checks whether the given domain is the one we create for the {@link ScriptingOperator}. If
* so, restrict what it can do.
*
* @param domain
* the domain in question, must not be {@code null}
* @return {@code true} if the domain is a groovy script; {@code false} otherwise
*
*/
private static boolean isGroovyScript(ProtectionDomain domain) {
if (domain.getCodeSource().getLocation() != null
&& domain.getCodeSource().getLocation().getPath().contains(ScriptingOperator.GROOVY_DOMAIN)) {
return true;
}
return false;
}
/**
* Checks whether the given domain is unknown. In that case, restrict everything!
*
* @param domain
* the domain in question, may be {@code null}
* @return {@code true} if the domain is unknown; {@code false} otherwise
*
*/
private static boolean isUnknown(ProtectionDomain domain) {
if (domain == null || domain.getCodeSource() == null) {
return true;
}
return false;
}
/**
* Create permission for unsigned extensions.
*
* @param loader
* the plugin class loader of the unsigned extensions
* @return the permissions, never {@code null}
*/
private static PermissionCollection createUnsignedPermissions(final PluginClassLoader loader) {
final Permissions permissions = new Permissions();
if (ProductConstraintManager.INSTANCE.isInitialized()) {
boolean isAllowed = ProductConstraintManager.INSTANCE.getActiveLicense()
.getPrecedence() >= StudioLicenseConstants.UNLIMITED_LICENSE_PRECEDENCE
|| ProductConstraintManager.INSTANCE.isTrialLicense();
boolean isEnabled = Boolean.parseBoolean(
ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_UPDATE_ADDITIONAL_PERMISSIONS));
if (isAllowed && isEnabled) {
permissions.add(new ReflectPermission("suppressAccessChecks"));
permissions.add(new ReflectPermission("newProxyInPackage.*"));
permissions.add(new AWTPermission("accessClipboard"));
permissions.add(new RuntimePermission("createClassLoader"));
permissions.add(new RuntimePermission("getClassLoader"));
permissions.add(new RuntimePermission("setContextClassLoader"));
permissions.add(new RuntimePermission("enableContextClassLoaderOverride"));
permissions.add(new RuntimePermission("closeClassLoader"));
permissions.add(new RuntimePermission("modifyThread"));
permissions.add(new RuntimePermission("stopThread"));
permissions.add(new RuntimePermission("modifyThreadGroup"));
permissions.add(new RuntimePermission("loadLibrary.*"));
permissions.add(new RuntimePermission("getStackTrace"));
permissions.add(new RuntimePermission("setDefaultUncaughtExceptionHandler"));
permissions.add(new RuntimePermission("preferences"));
permissions.add(new RuntimePermission("setFactory"));
permissions.add(new PropertyPermission("*", "write"));
}
}
permissions.add(new RuntimePermission("shutdownHooks"));
permissions.add(new PropertyPermission("*", "read"));
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
String userHome = System.getProperty("user.home");
String tmpDir = System.getProperty("java.io.tmpdir");
String pluginKey = loader.getPluginKey();
// delete access to the general temp directory
permissions.add(new FilePermission(tmpDir, "read, write"));
permissions.add(new FilePermission(tmpDir + "/-", "read, write, delete"));
// extensions can only delete files in their own subfolder of the
// .RapidMiner/extensions/workspace folder
if (pluginKey != null) {
String pluginFolder = pluginKey;
permissions.add(new FilePermission(userHome + "/.RapidMiner/extensions", "read"));
permissions.add(new FilePermission(userHome + "/.RapidMiner/extensions/workspace", "read"));
permissions.add(new FilePermission(userHome + "/.RapidMiner/extensions/workspace/" + pluginFolder,
"read, write"));
permissions.add(new FilePermission(userHome + "/.RapidMiner/extensions/workspace/" + pluginFolder + "/-",
"read, write, delete"));
}
// unfortunately currently we have to give all location permissons to read/write
// files to not block extensions that add "Read/Write xyz" operators
permissions.add(new FilePermission("<<ALL FILES>>", "read, write"));
return null;
}
});
addCommonPermissions(permissions);
return permissions;
}
/**
* Create permission for unknown sources.
*
* @return the permissions, never {@code null}
*/
private static PermissionCollection createUnknownSourcePermissions() {
Permissions permissions = new Permissions();
// empty permissions, not allowed to do anything
return permissions;
}
/**
* Create permission for groovy scripts of the {@link ScriptingOperator}.
*
* @return the permissions, never {@code null}
*/
private static PermissionCollection createGroovySourcePermissions() {
if (ProductConstraintManager.INSTANCE.isInitialized()) {
if (ProductConstraintManager.INSTANCE.getActiveLicense()
.getPrecedence() >= StudioLicenseConstants.UNLIMITED_LICENSE_PRECEDENCE
|| ProductConstraintManager.INSTANCE.isTrialLicense()) {
return createAllPermissions();
}
}
Permissions permissions = new Permissions();
// grant some permissions because the script is something the user himself created
permissions.add(new PropertyPermission("*", "read, write"));
permissions.add(new FilePermission("<<ALL FILES>>", "read, write, delete"));
addCommonPermissions(permissions);
return permissions;
}
/**
* Create permission for our trusted code. No restrictions are applied
*
* @return the permissions, never {@code null}
*/
private static PermissionCollection createAllPermissions() {
Permissions permissions = new Permissions();
permissions.add(new AllPermission());
return permissions;
}
/**
* Adds a couple of common permissions for both unsigned extensions as well as Groovy scripts.
*
* @param permissions
* the permissions object which will get the permissions added to it
*/
private static void addCommonPermissions(Permissions permissions) {
permissions.add(new AudioPermission("play"));
permissions.add(new AWTPermission("listenToAllAWTEvents"));
permissions.add(new AWTPermission("setWindowAlwaysOnTop"));
permissions.add(new AWTPermission("watchMousePointer"));
permissions.add(new LoggingPermission("control", ""));
permissions.add(new SocketPermission("*", "connect, listen, accept, resolve"));
permissions.add(new URLPermission("http://-", "*:*"));
permissions.add(new URLPermission("https://-", "*:*"));
// because random Java library calls use sun classes which may or may not do an acess check,
// we have to grant access to all of them
// this is a very unfortunate permission and I would love to not have it
// so if at any point in the future this won't be necessary any longer, remove it!!!
permissions.add(new RuntimePermission("accessClassInPackage.sun.*"));
permissions.add(new RuntimePermission("accessDeclaredMembers"));
permissions.add(new RuntimePermission("getenv.*"));
permissions.add(new RuntimePermission("getFileSystemAttributes"));
permissions.add(new RuntimePermission("readFileDescriptor"));
permissions.add(new RuntimePermission("writeFileDescriptor"));
permissions.add(new RuntimePermission("queuePrintJob"));
}
/**
* Verify the given certificates and see if at least one was signed by us.
*
* @param certificates
* the array of certificates to check
* @throws GeneralSecurityException
* if no certificate could be verified, will throw the last exception that occured
* during verification of all certificates. Can be {@code null}
*/
private static void verifyCertificates(Certificate[] certificates) throws GeneralSecurityException {
GeneralSecurityException lastException = null;
boolean verified = false;
for (Certificate certificate : certificates) {
try {
certificate.verify(key);
verified = true;
break;
} catch (GeneralSecurityException e) {
lastException = e;
}
}
if (!verified) {
throw lastException;
}
}
/**
* Checks whether the system property {@value #PROPERTY_SECURITY_ENFORCED} is set to
* {@code true}. This property is used to enable the full plugin sandbox security even on
* SNAPSHOT versions.
*
* @return {@code true} if the system property is set to 'true', {@code false} otherwise
*/
private static boolean isSecurityEnforced() {
// no need to synchronize, if this is entered multiple times it's fine
if (enforced == null) {
enforced = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return Boolean.parseBoolean(System.getProperty(PROPERTY_SECURITY_ENFORCED));
}
});
if (enforced) {
LogService.getRoot().log(Level.INFO,
"Plugin sandboxing enforced via '" + PROPERTY_SECURITY_ENFORCED + "' property.");
}
}
return enforced;
}
}