Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Security

How Do I Create My Own Security Manager?


Dr. Dobb's Journal June 1997: How Do I Create My Own Security Manager?

Cliff, vice president of technology of Digital Focus, can be contacted at [email protected]. To submit questions, check out the Java Developer FAQ Web site at http://www.digitalfocus.com/faq/.


Java is quite possibly the most secure widely-available environment you can program in. This month, I'll examine one aspect of the Java security system -- how to create a security manager for your applications. (For a general discussion of Java security, see Java Security, by Gary McGraw and Ed Felten, John Wiley & Sons, 1996.)

Why would you want to create a security manager? The answer to this question depends on the application. Most applications that execute on the host on which they reside do not require a security manager. The host system's built-in security features restrict the activities of the process in which the Java virtual machine is running, and that process is no more inherently dangerous than any other process users might execute. A security manager is desirable primarily in applications in which the Java application originates from an untrusted source (usually external to the machine). In the example I present here, Java code (potentially from an untrusted source) is dynamically compiled and loaded into the program. This might occur in a system that performs certain functions for users on a network, allowing those users to add their own functions while the system is running. A security manager is highly recommended in such an application because the code that is dynamically added is installed directly in a running environment and may unintentionally corrupt the environment.

The SecurityManager Class

The core of the Java run-time's security system is the SecurityManager class. This class acts as a monitor of any and all activities that are potentially a breach of security. A running application is permitted to have one and only one security manager. This object must extend SecurityManager, and once a security manager is installed by the program, it may not be uninstalled or replaced. This is enforced by the Java virtual machine. A browser such as Netscape has its own security manager class, and installs it into the virtual machine it uses to run applets. In this way, the browser protects your computer from errant or malicious applets.

The SecurityManager class acts as a gatekeeper. Every time a Java program attempts to write to a file, open a network socket, or even exit the virtual machine, final code within the Java run-time environment asks the installed security manager if it has permission. For example, if your program attempts to call System.exec(String) to fork a process, the method SecurityManager.checkExec(String) is automatically called, and the security manager has an opportunity to decide if the program should be allowed to perform the exec. This plugs an important security loophole for programs that originate from untrusted sources, since an exec can potentially access the user's file system and read sensitive data (or in some environments even put the system into a supervisor state).

To write a security manager, users extend the java.lang.SecurityManager class, and implement the methods in that class to reflect a security policy appropriate for the application and the application's intended deployment. There is a catch, however. Most methods in the security-manager class do not pass in enough information to make more than a trivial decision about whether or not to allow the intended operation. For example, the checkRead(String) method merely takes a filename. While this is sufficient if you want to restrict all file reads (that is, simply return False, no matter what) or if the right to access can be determined from the name, it is not sufficient if access rights are determined from other factors in addition to the filename. When code is dynamically compiled and loaded, you would like the main application to have unrestricted access to the system while restricting user-installed code to reading/writing within a particular directory.

To do this, you can take advantage of another feature of the Java security system -- thread groups. Thread groups are hierarchical collections of threads, grouped together by the programmer for convenience. Thread groups may be used to simplify synchronization in complex multithreaded systems. Another use of thread groups is to group together a set of threads that must be restricted in some way. Just as for other resources, the Java virtual machine asks the security manager (if there is one) for permission whenever a thread attempts to create or alter the execution of another thread. Thus, if the installed security manager is designed to restrict thread operations to subordinate groups, an errant thread cannot create a new thread in a nonsubordinate thread group. This means that the thread-group hierarchy can be protected and, therefore, used to determine a pecking order of privilege for access to other resources: The security manager can determine the thread group to which the currently running thread belongs, and based on that determination, decide what rights the thread has. To utilize this, you run the dynamically compiled code in a subordinate thread group, and implement the appropriate thread group access permission methods in the security manager.

GremlinWatcher

GremlinWatcher is a simple security manager that implements a policy of allowing threads in the application's main thread group to have unrestricted access to the system (see Figure 1). However, it prevents other threads from reading from or writing to files that have the file extension ".private", or those which are outside of a predesignated Gremlin directory.

First, GremlinWatcher implements the SecurityManager's checkAccess(Thread) and checkAccess(ThreadGroup) methods. In these methods, you check if the current thread's thread group is either a parent of or identical to the thread or thread group argument; if not, you throw a security exception. This protects your thread groups so that you can run Gremlin threads in their own thread group without fear that they can create new threads in other higher-level thread groups, thereby defeating our security hierarchy.

Next, GremlinWatcher implements the SecurityManager class's checkRead() and checkWrite() methods. Each of these methods first checks if the current thread is a member of the main routine's thread group; if so, it returns, thereby allowing the read. If not, it checks whether or not the filename passed as a parameter is outside the Gremlin directory and whether the filename ends with the string ".private". If either of these conditions is true, checkRead() or checkWrite() throws a SecurityException, vetoing the read/write request.

Dynamic Compilation

When you run the JDK's javac compiler from the command line, it loads a Java virtual machine and runs the public class sun.tools.javac.Main, which instantiates an instance of sun.tools.javac.Main and calls its compile() method, with the source filenames passed as arguments. You can instantiate the Main class in any Java program and call the compile() method in the same way. This means that you can write programs that compile other programs, using the JDK compiler. The compiled classes can then be loaded into the running program with a class loader of your own construction, and their methods executed. To load a class with a class loader, you instantiate the class loader and call ClassLoader.loadClass(classname,true). As long as the class has a default constructor (one with no parameters), this method will work. In Listing One I dynamically compile a file called "Test.java" (available electronically; see "Availability," page 3), which contains a class called Test.

To instantiate the class just compiled, you call a class loader. Since a Java application (unlike an applet run in a browser, or a remote method invocation) does not have a class loader, you have to provide one. My class loader, called GremlinLoader, opens the class file, reads it as a stream of bytes, and calls ClassLoader.defineClass(bytes,0,bytes.length) (in Java 1.1, this method is replaced with a method that requires an extra class name parameter), followed by resolveClass(class). It then returns the newly loaded class. To create an instance of this class, I call Class.newInstance() on the newly loaded Class object; see Figure 2.

For security reasons, you want this class to execute in a child thread group, which can be monitored by our security manager. Therefore, I create a thread group called "Gremlins," and create a new thread in that group by instantiating a new Thread(gremlins,test), where test is the instance of the newly loaded class that we instantiated by calling newInstance(). I make the dynamically compiled class implement the java.lang.Runnable interface, so that you can run it in a thread. Now that you have a thread to run it in, you can start the thread by calling the Thread start() method. This tells the Thread object to run the dynamically-compiled class in a thread in the Gremlin thread group, with the Runnable.run() method as its entry point.

When the dynamically compiled class is run, the VM attempts to load any classes that it needs using the class loader that loaded the dynamically compiled class. This is your class loader. When the class loader attempts to perform a read or an existence test on the class, the security manager is invoked with a checkRead(filename) call. For example, if the dynamically compiled class uses java.lang.System, your security manager will receive a call to checkRead("java.lang.System.class"). In my implementation, I simply construct a File object from the filename parameter, and since java.lang.String.class is a relative filename, this results in a file with the full path my-current-directory/java.lang.System.class. If the current directory is in the allowed Gremlin work area, the security manager will approve the read request; otherwise it will throw a security exception. This means that the class loader has to be prepared to catch this exception.

When my class loader attempts to load a class, it performs an existence test on the class before loading it. This existence test results in a checkRead() call to the security manager and may result in an exception. Since I am merely trying to load a class, I do not want the exception to propagate, so I catch it and simply return null. This is a signal to the run time that the class loader failed and the classpath should be searched for the class file instead. When the VM reads classpath directories for a class file, it does not ask the security manager for permission, since this is a built-in operation that cannot be overridden by a user program. Therefore, the security manager will not be called again to verify permission to read the class file.

There are other ways besides using thread groups to determine access rights to system resources. For example, the SecurityManager base class provides native methods that a derived security manager may call to determine whether the execution context is that of the main class or the class of an invoked method. The SecurityManager.classDepth(String classname) method returns the position of the stack frame containing the first occurrence of the named class. This can be used to see if you are above or below the main program class in the current thread's stack, or if the thread's stack even contains the main program class.

The complete source code for the security manager, class loader, and demo program is available electronically from DDJ and at http://www.digitalfocus.com/ddj/code/.

Summary

The Java run-time security model is effective, robust, and flexible. Security features can easily be customized for an application and made to incorporate considerations unique to the application's deployment environment. It can even be made to be on a per-user basis. This is in sharp contrast with all-or-nothing alternatives. In a future column, I will examine the JDK 1.1's newer security features, including Java Electronic Commerce Framework and signed applets. Perhaps most importantly, these security features are vendor independent -- vendor-specific solutions are suspect given the track record of vendors when it comes to the sanctity of the user's data and applications. Java respects that sanctity.

DDJ

Listing One

/* Copyright Digital Focus. This code may be used for non-commercial purposes. * No guarantee or warrantee is given or implied.
 */


/** Our security manager. */ class GremlinWatcher extends SecurityManager { private ThreadGroup seniorThreadGroup; private String gremlinDirectory; public GremlinWatcher(ThreadGroup seniorThreadGroup, String gremlinDirectory) { super(); this.seniorThreadGroup = seniorThreadGroup; this.gremlinDirectory = gremlinDirectory; } public void checkCreateClassLoader() { } public void checkAccess(Thread t) { ThreadGroup g = t.getThreadGroup(); checkAccess(g); } public void checkAccess(ThreadGroup g) { ThreadGroup tg = Thread.currentThread().getThreadGroup(); if (! tg.parentOf(g)) throw new SecurityException(); } public void checkExit(int status) { } public void checkExec(String cmd) { } public void checkLink(String lib) { } public void checkRead(java.io.FileDescriptor fd) { } public void checkRead(String file) { ThreadGroup tg = Thread.currentThread().getThreadGroup(); if (tg != seniorThreadGroup) { // Perform checks; if checks fail, throw exception java.io.File f = new java.io.File(file); String path = f.getAbsolutePath(); if (! path.regionMatches(true, 0, gremlinDirectory, 0, gremlinDirectory.length())) throw new SecurityException("Cannot read file (" + path + ") outside of Gremlin Directory"); if (f.getName().regionMatches(true, f.getName().length() - 8, ".private", 0, 8)) throw new SecurityException("Cannot read private file (" + path + ")"); } // return ok System.out.println("File " + file + " may be read"); } public void checkRead(String file, Object context) { } public void checkWrite(java.io.FileDescriptor fd) { } public void checkWrite(String file) { ThreadGroup tg = Thread.currentThread().getThreadGroup(); if (tg != seniorThreadGroup) { // Perform checks; if checks fail, throw exception java.io.File f = new java.io.File(file); String path = f.getAbsolutePath(); if (! path.regionMatches(true, 0, gremlinDirectory, 0, gremlinDirectory.length())) throw new SecurityException("Cannot write file (" + path + ") outside of Gremlin Directory"); if (f.getName().regionMatches(true, f.getName().length() - 8, ".private", 0, 8)) throw new SecurityException("Cannot write private file (" + path + ")"); } // return ok System.out.println("File " + file + " may be written"); } public void checkDelete(String file) { } public void checkConnect(String host, int port) { } public void checkConnect(String host, int port, Object context) { } public void checkListen(int port) { } public void checkAccept(String host, int port) { } public void checkMulticast(java.net.InetAddress maddr) { } public void checkMulticast(java.net.InetAddress maddr, byte ttl) { } public void checkPropertiesAccess() { } public void checkPropertyAccess(String key) { } public void checkPropertyAccess(String key, String def) { } public boolean checkTopLevelWindow(Object window) { return true; } public void checkPrintJobAccess() { } public void checkSystemClipboardAccess() { } public void checkAwtEventQueueAccess() { } public void checkPackageAccess(String pkg) { } public void checkPackageDefinition(String pkg) { } public void checkSetFactory() { } public void checkMemberAccess(Class clazz, int which) { } public void checkSecurityAccess(String provider) { } } /** Our main class. */ public final class Demo { private static ThreadGroup gremlins; private static boolean running = false; private Demo() {} // Do not allow someone to instantiate /** Compiles a file called "Test.java" in the current directory, which * contains a class called "Test", and dynamically loads that class and * runs its run() method in a thread. Class Test must implement Runnable. * The only argument is the absolute path of the directory that Gremlins * have unrestricted access to. */ public static void main(String[] args) { if (running) return; running = true; SecurityManager sm = new GremlinWatcher(Thread.currentThread().getThreadGroup(),args[0]); System.setSecurityManager(sm); gremlins = new ThreadGroup("Gremlins"); sun.tools.javac.Main compiler = new sun.tools.javac.Main(System.out, "javac"); GremlinLoader cl = new GremlinLoader(); try { System.out.println("Compiling..."); String[] file = { "Test.java" }; if (! compiler.compile(file)) return; System.out.println("...compilation completed."); System.out.println("Loading class..."); Class c = null; System.out.println("Calling loadClass()..."); c = cl.loadClass("Test", true); System.out.println("...loadClass() completed."); System.out.println("...loaded."); System.out.println("Creating new instance..."); Runnable test = (Runnable)c.newInstance(); System.out.println("...new instance created."); // Create a new thread for test, in the gremlin's thread group. // Within that thread group, they can do whatever they want! Thread t = new Thread(gremlins, test); t.start(); } catch (Exception e) { e.printStackTrace(); } } } /** Class loader, for loading classes we generate dynamically at run time. */ class GremlinLoader extends ClassLoader { public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { System.out.println("Attempting to load class " + name); Class c; try { String fname = name + ".class"; java.io.File file = new java.io.File(fname); if (! file.exists()) { System.out.println("Loader could not find class " + name); c = findLoadedClass(name); // comment out for JDK 1.02 if (c == null) c = findSystemClass(name); // comment out for JDK 1.02 if (resolve) resolveClass(c); // comment out for JDK 1.02 return c; // comment out for JDK 1.02 //return null; // uncomment this for JDK 1.02 } System.out.println(" (fname=" + fname + ")"); java.io.FileInputStream f = new java.io.FileInputStream(fname); byte[] bytes = new byte[(int)file.length()]; int n = f.read(bytes); System.out.println(" (read " + n + " bytes)"); c = defineClass(bytes, 0, bytes.length); System.out.println(" (class defined)"); if (resolve) resolveClass(c); System.out.println(" (class resolved)"); return c; } catch (ClassNotFoundException cex) { c = findLoadedClass(name); if (resolve) resolveClass(c); return c; } catch (Exception ex) { System.out.println("Loader could not load " + name); ex.printStackTrace(); return null; } } }

Back to Article


Copyright © 1997, Dr. Dobb's Journal

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.