Tuesday, September 9, 2014

Plugins and Plugin Security Basics

So I thought it would be fun to create a simple plugin ..... framework? (framework sounds too big for what I did) Regardless, I wanted to play around with dynamically loading classes into the JVM and running code in them just to see what's involved.

So to start you need to create an interface for the plugin classes to implement. This is pretty simple, I'm using Java 8 so my interface also has a default method that controls how the plugin functions. Here is the interface:

public interface Plugin
{
default public void run() {
setup();
performAction();
tearDown();
}
public void setup();
public void tearDown();
public void performAction();
}

So the implementing classes just have to define any setup code needed and then do something in the performAction method. Obviously this is very simple and doesn't lend it self to very dynamic or extensive plugins but it let me learn what I wanted and gets us started.

Now the interesting code, we have to dynamically load code from JAR's (or just class files, doesn't matter), this function will search the "." directory for any .jar files and load any classes in the jar file and if they are instances of our Plugin interface the class will get stored in a list that gets returned:

public List<Plugin> loadPlugins() {

   private List<Plugin> plugins = new ArrayList<Plugin>();

   FileFilter jarFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().toUpperCase().endsWith("JAR");
}
   };

   File filePath = new File(".");
   File[] files = filePath.listFiles(jarFilter);
   List<File> filesList = Arrays.stream(files).collect(Collectors.toList());
   int size = filesList.size();

   for(int i = 0; i < size; i++) {

File file = filesList.get(i);

if(file.isFile()) {
try {
List<String> classNames = new ArrayList<String>();
ZipInputStream zip = null;
try {
zip = new ZipInputStream(new FileInputStream(file.getAbsolutePath()));

for (ZipEntry entry = zip.getNextEntry(); entry != null; entry =                    
                      zip.getNextEntry()) {

if (entry.getName().endsWith(".class") && !entry.isDirectory()) {
classNames.add(entry.getName().substring(0,entry.getName().length()-
                         ".class".length()));
}
}
} finally {
zip.close();
}

URL[] urls = {new URL("jar:file:" + file.getAbsolutePath() + "!/")};
MyLoader loader = null;
try {
loader = new MyLoader(urls, this.getClass().getClassLoader());
try {
for(String className : classNames) {

className = className.replace("/",".");
Class cls = loader.loadClass(className);

Object classInstance = cls.newInstance();
if(classInstance instanceof Plugin) {

plugins.add((Plugin)classInstance);
}
}
} catch(ClassNotFoundException e) {
e.printStackTrace();
} catch(IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
} finally {
loader.close();
}
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
}
}
   }

   return plugins;
}

Some interesting parts to point out include the fact that when loading a ZipInputStream you can get entries from it and just check the extension on each entry, that's almost too easy!
The cool part here is the loader, this object takes those class entries from the Jar file and loads them into the JVM so that we can run code on them, you can see we call cls.newInstance() which functions the same as a call to new Object().
The MyLoader class is pretty simple, it just extends the URLClassLoader and loads classes from the jar file. It tries to load the class locally, if that fails, it checks if the class has already been loaded and if that fails it falls back to loading the class normally. (This is code I found on someone elses project, check it out at https://github.com/decebals/pf4j/)

private class MyLoader extends URLClassLoader {
 
public MyLoader(URL[] urls, ClassLoader parent) {

super(urls, parent);
}

/**
    * This implementation of loadClass uses a child first delegation model
    * rather than the standard parent first.
   * If the requested class cannot be found in this class loader, the parent
    * class loader will be consulted
   * via the standard ClassLoader.loadClass(String) mechanism.
   */
@Override
   public Class<?> loadClass(String className) throws ClassNotFoundException {
       
       try {
           return getClass().getClassLoader().loadClass(className);
       } catch (ClassNotFoundException e) {
       }
 
       // second check whether it's already been loaded
       Class<?> clazz = findLoadedClass(className);
       if (clazz != null) {
            return clazz;
       }

       // nope, try to load locally
       try {
           clazz = findClass(className);
           return clazz;
       } catch (ClassNotFoundException e) {
        // try next step
       }

       // use the standard URLClassLoader (which follows normal parent delegation)
       return super.loadClass(className);
}

public void close() throws IOException
{
super.close();
}
}

So now that we have the Plugin classes loaded into the running JVM we can actually do something with them. For my simple purposes all I did was use a for loop to execute the run method.

PluginManager pm = new PluginManager();
List<Plugin> plugins = pm.loadPlugins();
m.setAllowAll(false);
for(Plugin plugin : plugins) {

plugin.run();
}

Done, simple plugin framework!

So, there are some interesting things to keep in mind at this point. First off you need to realize that each plugin loaded has full trusted code permissions. Since we haven't done anything to explicitly limit the permissions the plugins have they have all permissions the rest of the code has. Who cares? Well if this were some kind of external system where we let anyone create and add plugins then they could create a plugin that could load other classes, access file systems, shut down the JVM, access databases, really anything that your trusted code can do. For example I created a simple plugin that when run will shut down the JVM running the PluginManager.

public class PluginTest implements Plugin{

@Override
public void setup() {
try {
SecurityManager m = System.getSecurityManager();
if(m != null) {
m.checkPermission(new RuntimePermission("createClassLoader"));
} else {
System.out.println("no security manager");
          }
System.out.println("I can create class loaders");
} catch (Exception e) {

System.out.println("Unable to create class loaders in unsecured code");
}

try {
SecurityManager m = System.getSecurityManager();
if(m != null) {
m.checkPermission(new RuntimePermission("exitVM.{3}"));
} else {
System.out.println("no security manager");
}
System.out.println("I can shut down the system");
} catch (Exception e) {

System.out.println("Unable to exit in unsecured code");
}

System.out.println("Setup Plugin 1");

   }

@Override
public void tearDown(){
try {
SecurityManager m = System.getSecurityManager();
if(m != null) {
m.checkPermission(new RuntimePermission("exitVM.{3}"));
} else {
System.out.println("no security manager");
}
System.out.println("system shut down started");
System.exit(3);
} catch (Exception e) {

System.out.println("Unable to exit in unsecured code");
}
System.out.println("Tear Down Plugin 1");

}

@Override
public void performAction() {
System.out.println("Plugin ACTION!!!!");
}
}

You can see I check for a Security Manager and see if I have permissions to two different risky methods and then in the tearDown method I actually try to shut down the JVM. All the Try/Catches are to make sure the plugin keeps running in case it doesn't have permissions to do it's dastardly deeds.

So what can we do to protect our trusted code from being hacked by this malicious plugin? Well for starters we can add a Security Manager to the running JVM before any plugins are run. There are some problems with this. First you can only set the Security Manager one time. So we can't add a Security Manager before running the plugins and then remove it when we are done. We don't want to just add one at the beginning that has all permissions locked down or we severely limit what our trusted code can do. So really we want to turn up the security before running the plugins then turn it back down once we've ensured all the plugins are finished running. Some limits need to be considered with threads, a malicious plugin could try to start a thread that waits until it has permission before doing something. I'm not going to address this or many other side cases, just the basics here. Perhaps in a future post I'll dive in a bit deeper.
So for my purposes I created a simple Security Manager that allows my to turn on and off all permissions.

public class MySecurityManager extends SecurityManager {
private boolean allowAll = false;

public void setAllowAll(boolean value) {
allowAll = value;
}

@Override
public void checkPermission(Permission perm) {
if(!allowAll) {

super.checkPermission(perm);
}
}
}

Now my code that runs the plugins changes to the following:

MySecurityManager m = new MySecurityManager();
System.setSecurityManager(m);

m.setAllowAll(true);

PluginManager pm = new PluginManager();
List<Plugin> plugins = pm.loadPlugins();

m.setAllowAll(false);
for(Plugin plugin : plugins) {

plugin.run();
}
m.setAllowAll(true);

Now the plugins have no security permissions while they are running, they can't access the file system, they can't call methods like exit, they can't do anything I setup my Security Manager to prevent. The Java API lists the various available permissions that the Security Manager handles.
(http://docs.oracle.com/javase/7/docs/api/java/lang/SecurityManager.html SecurityPermission and RuntimePermission list lots of interesting permissions.)

Find a slightly refactored version of the complete project at https://github.com/hardyc3/SimplePluginFramework

No comments:

Post a Comment