Third party mod sandboxing

I am wanting to be able to allow third party mods to be pluged into my game however since i cannot garentee that the mods will not access the local disk or do other bad stuff, is it possible to ‘sandbox’ the code of the mod?

Basically i would want to allow the mod code the same functionality as would an applet.

Hi

This can be done with creating an extension of a Classloader that implements the loading of the third party plugin and the security constrains you like to set for the plugins is implemented in an extension of an SecurityManager.

I created such and plugin based system for a game (that of course never was completed). In the game, events could occure in with random time intervall. Some events was that the missile you attack someone with hit Santa instead and the all children didn’t get their christmas presents. Events like these where created by extending a know interface. And I thought that other people would like to help create such events so I created the plugin based system.

As security constratins I choosed the following:

  • All implemented events must lie in the package org.backmask.nuclearwar.randomevents.impl
  • Events can only read files under the directory the game is installed
  • Events can only write to files under the directory randomevents that lies directly under the directory the game is installed.

Some extra demands I set

  • All events plugin must be zipped into a file called .nwjar. The file shall contain all graphics/sounds and classes needed to exceute the event.
  • The nwjar file must have a meta-inf/Manifest.mf file specifing what class implements the event Interface.

And here is the code. If you have question about anything ask.

SecurityManager


package org.backmask.nuclearwar.security;

import java.io.File;
import java.io.FilePermission;
import java.security.Permission;
import java.util.PropertyPermission;

/**
 * @author P950MBC
 *
 * To change this generated comment edit the template variable "typecomment":
 * Window>Preferences>Java>Templates.
 * To enable and disable the creation of type comments go to
 * Window>Preferences>Java>Code Generation.
 */
public class NuclearWarSecurityManager extends SecurityManager {

    public void checkPermission(Permission perm){
        checkPermission(perm, null);
    }

    public void checkPermission(Permission perm, Object context){

            //if the permission orginates from a untrusted randomevent, grant permission if not
            String className = isRandomEvents();
          if (className == null)
                return;
                
        String action = perm.getActions();
        String name = perm.getName();
       
            //A untrusted randomevents is trying to access the file system
            if(perm instanceof FilePermission){

                  //Make sure that the file that are being accessed is under the user.dir            
            String pwd = (new File(System.getProperty("user.dir"))).getAbsolutePath();
            String abspath = (new File(name)).getAbsolutePath();
            
            if(!(abspath.startsWith(pwd)/* || abspath.toUpperCase().startsWith(javahome.toUpperCase())*/))
                  fail(perm, context);

                  //Untrusted randomevents can read any file in the Nuclear War folder
            if(action.equals("read"))
                return;
            
            //Untrusted randomevents don´t have write access anywhere but the randomevents folder
            //and only to the events zip file(event.zip) and the events propertyfile(event.property).
            if(action.equals("write")){
                  
                  //Check if the file is in the randomevents subfolder
                  pwd += File.separator + "randomevents";
                  if (!abspath.startsWith(pwd))
                        fail(perm, context);
                  
                  String fileName = (new File(name)).getName();
                  String ext = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
                  fileName = fileName.substring(0, fileName.lastIndexOf("."));
                  
                  className = className.substring(className.lastIndexOf(".") + 1, className.length());

                        //Controlls that the file has the right name and the right extensions
                        if (fileName.equals(className) && (ext.equals("zip") || ext.equals("property")))
                              return;
                  
                fail(perm, context);
            }
        }
        
       if(perm instanceof PropertyPermission){
            if(action.equals("read"))
                return;
            if(action.equals("write"))
                fail(perm, context);
        }        
                
          System.out.println("<<<< Failing permission " + perm);
          fail(perm, context);
    }
    
    
    
    
    private String isRandomEvents(){
          Class classes[] = getClassContext();
          for (int i = 0; i < classes.length; i++){
                if (classes[i].getName().startsWith("org.backmask.nuclearwar.randomevents.impl.")){
                      return classes[i].getName();
                }
          }
          return null;
    }


    private final void fail(Permission perm, Object context){
        throw new SecurityException(makeMessage(perm, context));
    }
    
    private final String makeMessage(Permission perm, Object context){
        String classname = perm.getClass().getName();
        String action = perm.getActions();
        String name = perm.getName();
        StringBuffer sb = new StringBuffer(80);
        sb.append(classname).append('.').append(action);
        sb.append('(').append(name).append("), context=").append(context);
        Class classctx[] = getClassContext();
        for(int i = 0; i < classctx.length; i++){
              String ctxname = classctx[i].getName();
                sb.append("\n    ").append(ctxname);
        }
        return sb.toString();
    }    
}

Classloader


package org.backmask.nuclearwar.security;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.backmask.util.file.ZipFiles;

/**
 * @author Administrator
 *
 * To change this generated comment edit the template variable "typecomment":
 * Window>Preferences>Java>Templates.
 * To enable and disable the creation of type comments go to
 * Window>Preferences>Java>Code Generation.
 */
public class NuclearWarClassLoader extends ClassLoader {

      private Hashtable cache = null;
      private Vector repository = null;
      private String validPackageName = null;
      private boolean allowAll = false;

      public NuclearWarClassLoader(Vector eventRepository, String validPackageName, boolean allowAll) {
            //Create cache
            cache = new Hashtable();
            this.validPackageName = validPackageName;
            this.allowAll = allowAll;
            
            //Check that all entries in the repository so they are valid
        Enumeration e = eventRepository.elements();
        while(e.hasMoreElements()) {
            Object o = e.nextElement();
            File file;

            // Check to see if element is a File instance.
            try {
                file = (File) o;
            } catch (ClassCastException objectIsNotFile) {
                throw new IllegalArgumentException("Object " + o
                    + "is not a valid \"File\" instance");
            }

            // Check to see if we have proper access.
            if (!file.exists()) {
                throw new IllegalArgumentException("Repository "
                    + file.getAbsolutePath() + " doesn't exist!");
            } else if (!file.canRead()) {
                throw new IllegalArgumentException(
                    "Don't have read access for file "
                     + file.getAbsolutePath());
            }

                  // Check that the file has the correct file ending
                  if (!file.getName().endsWith(".nwjar"))
                throw new IllegalArgumentException("Only valid Nuclear War jar files can be used as repositories");

            // Check that it is a zip/jar file
            if (!ZipFiles.isZipOrJarArchive(file)) {
                throw new IllegalArgumentException(file.getAbsolutePath()
                    + " is not a directory or zip/jar file"
                    + " or if it's a zip/jar file then it is corrupted.");
            }
        }
        System.out.println("classloader repository contains " + eventRepository.size() + " valid entries");

        // Store the class repository for use
        this.repository = eventRepository;            
      }
      
      private byte[] getClassImplFromZipFile(String className){
            try{
                // Try to load it from each repository
                Enumeration repEnum = repository.elements();
            
                while (repEnum.hasMoreElements()) {
                    byte[] classData;
            
                    File file = (File) repEnum.nextElement();
                    System.out.println("Searching in file " + file.getAbsolutePath());
                    try {
                          classData = loadClassFromZipfile(file, className);
                    } catch(IOException ioe) {
                        // Error while reading in data, consider it as not found
                        classData = null;
                    }
                    
                    if (classData != null)
                          return classData;
                }
            }catch (Exception e){
                  /*
                   * If we caught an exception, either the class wasnt found or it
                   * was unreadable by our process.
                   */                  
                  return null;
            }
            return null;
      }

    /**
     * Tries to load the class from a zip file.
     *
     * @param file The zipfile that contains classes.
     * @param name The classname
     * @param cache The cache entry to set the file if successful.
     */
    private byte[] loadClassFromZipfile(File file, String name)
        throws IOException
    {
        // Translate class name to file name
        String classFileName = name.replace('.', '/') + ".class";

        ZipFile zipfile = new ZipFile(file);

        try {
            ZipEntry entry = zipfile.getEntry(classFileName);
            if (entry != null) {
                  System.out.println("        >>>>>> Found class in zip file : " + zipfile.getName());
                return loadBytesFromStream(zipfile.getInputStream(entry), (int) entry.getSize());
            } else {
                  System.out.println("        ****** Did not found class : " + classFileName + " in zip file : " + zipfile.getName());
                // Not found
                return null;
            }
        } finally {
            zipfile.close();
        }
    }

    /**
     * Loads all the bytes of an InputStream.
     */
    private byte[] loadBytesFromStream(InputStream in, int length)
        throws IOException
    {
        byte[] buf = new byte[length];
        int nRead, count = 0;

        while ((length > 0) && ((nRead = in.read(buf,count,length)) != -1)) {
            count += nRead;
            length -= nRead;
        }

        return buf;
    }


      /**
       * This is a simple version for external clients since they
       * will always want the class resolved before it is returned
       * to them.
       */
      public Class loadClass(String className) throws ClassNotFoundException {
            return (loadClass(className, true));
      }

      /**
       * This is the required version of loadClass which is called
       * both from loadClass above and from the internal function
       * FindClassFromClass.
       */
      public synchronized Class loadClass(String className, boolean resolveIt)
            throws ClassNotFoundException {
            Class result;
            byte classData[];

            System.out.println("        >>>>>> Load class : " + className);

            /* Check our local cache of classes */
            result = (Class) cache.get(className);
            if (result != null) {
                  System.out.println("        >>>>>> returning cached result.");
                  return result;
            }

            /* Check with the primordial class loader */
            if (securityAllowsClass(className)){
                  try {
                        result = super.findSystemClass(className);
                        System.out.println("        >>>>>> returning system class (in CLASSPATH).");
                        return result;
                  } catch (ClassNotFoundException e) {
                        System.out.println("        >>>>>> Not a system class.");
                  }
            }

            if (!allowAll)
                  if (!className.startsWith(validPackageName)) {
                        System.out.println("        >>>>>> Not a valid package name. (" + validPackageName + ")");
                        throw new ClassNotFoundException("Not a valid package name.");
                  }

            /* Try to load it from our repository */
            classData = getClassImplFromZipFile(className);
            if (classData == null) {
                  System.out.println("        >>>>>> Did not found class in zip file.");
                  throw new ClassNotFoundException("Couldn&#180;t found class");
            }

            /* Define it (parse the class file) */
            result = defineClass(className, classData, 0, classData.length);
            if (result == null) {
                  throw new ClassFormatError("Class format error");
            }

            if (resolveIt) {
                  resolveClass(result);
            }

            cache.put(className, result);
            System.out.println("        >>>>>> Returning newly loaded class.");
            return result;
      }
      
    /**
     * Checks whether a classloader is allowed to define a given class,
     * within the security manager restrictions.
     */
    private boolean securityAllowsClass(String className) {
        try {
            SecurityManager security = System.getSecurityManager();

            if (security == null) {
                // if there's no security manager then all classes
                // are allowed to be loaded
                return true;
            }

            int lastDot = className.lastIndexOf('.');
            // Check if we are allowed to load the class' package
            security.checkPackageDefinition((lastDot > -1) ? className.substring(0, lastDot) : "");
            // Throws if not allowed
            return true;
        } catch (SecurityException e) {
            return false;
        }
    }
    
    public InputStream getResourceAsStream(String name) {
        // Try to load it from the system class
        InputStream s = getSystemResourceAsStream(name);

        if (s == null) {
            // Try to find it from every repository
            Enumeration repEnum = repository.elements();
            while (repEnum.hasMoreElements()) {
                File file = (File) repEnum.nextElement();
                s = loadResourceFromZipfile(file, name);

                if (s != null) {
                    break;
                }
            }
        }

        return s;
    }

    /**
     * Loads resource from a zip file
     */
    private InputStream loadResourceFromZipfile(File file, String name) {
        try {
            ZipFile zipfile = new ZipFile(file);
            ZipEntry entry = zipfile.getEntry(name);

            if (entry != null) {
                return zipfile.getInputStream(entry);
            } else {
                return null;
            }
        } catch(IOException e) {
            return null;
        }
    }
}

Set up the SecurityManager


    public void main(String[] args){
        System.setSecurityManager(new NuclearWarSecurityManager());
        YourGame game = new YourGame();
         game.start();
    }

And finally load the plugin


            //Find all console actions zip files
            Vector filelist = new Vector();
            Directory.getFileList(new File("randomevents/"), "nwjar", filelist);

            Vector mainClasses = new Vector();

            for (int i=0; i<filelist.size(); i++){
                  File f = (File) filelist.elementAt(i);
                  InputStream in = null;
                  try{
                        in = ZipFiles.getZipFileEntry(f, "meta-inf/Manifest.mf");
                        
                        if (in == null){
                              System.err.println("Couldn't find fil meta-inf/Manifest.mf in file " + f.getName() + ". ConsoleAction not loaded");
                              filelist.remove(i);
                        }else{
                              //read file
                              BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                              boolean done = false;
                              do{
                                    String str = reader.readLine();
                                    if (str == null){
                                          done = true;
                                          System.err.println("Couldn't find Main-class argument in meta-inf/Manifest.mf in file " + f.getName() + ". ConsoleAction not loaded");
                                          filelist.remove(i);
                                    }else if (str.startsWith("Main-class:")){
                                          mainClasses.add(str.substring(str.indexOf(":")+1).trim());
                                          done = true;
                                    }
                              }while (!done);
                        }
                        
                  }catch (IOException io){
                        io.printStackTrace();
                        filelist.remove(i);
                  }
            }
            
            //create classloader
            classLoader = new NuclearWarClassLoader(filelist, PACKAGE_NAME, true);
            
            for (int i=0; i<filelist.size(); i++){
                  File f = (File) filelist.elementAt(i);
                  String name = (String) mainClasses.get(i);
                  log.fine("loading class: " + name);
                  
                  try{
                        //load actions
                        Class c = classLoader.loadClass(name);
                        Object o = c.newInstance();
                        RandomEvent ca = (RandomEvent) o;
                        
                        randomEvents.add(ca);
                        
                  }catch (ClassNotFoundException e){
                        e.printStackTrace();
                  }catch (ClassCastException e){
                        e.printStackTrace();
                  }catch (IllegalAccessException e){
                        e.printStackTrace();
                  }catch (InstantiationException e){
                        e.printStackTrace();
                  }catch (InvalidParameterException e){
                        e.printStackTrace();
                  }catch (Exception e){
                        e.printStackTrace();
                  }
                  
            }

Thanx! that definitely gives me code to mull over :slight_smile:

;D Just start to play with it. Any questions post them here as my email account isnt working right now, so that you know.

Backmask thanks a lot!

Your welcome… always happy to share some code