6.3. Implementation TechniquesWe'll now turn our attention to implementing security policies. Our goal is to show how to write a security manager--one that can be used in conjunction with the access controller, and one that can stand alone. We'll plug these security managers into our JavaRunner program, and we'll also discuss the implementation of the security manager that comes with the Launcher and how that security manager may be installed. 6.3.1. Utility ClassesIn order to make our implementation of the security manger a bit easier, we'll provide a few utility classes. As we intimated above, there are many times when we want to reject an operation if there is any untrusted class on the stack. In order to simplify this operation, we define this method: Class Definitionprivate void checkClassLoader(String ask, String ex) { // Use the ask string to prompt the user if the operation // should succeed if (inClassLoader()) { throw new SecurityException(ex); } } We've passed a string to this method that allows us to ask the user if the operation in question should be permitted; for example, the application could pop up a dialog window and give the user the opportunity to accept the operation. Whether or not that ability is a good idea is open to debate; we've left it to the reader to provide the logic to implement that feature (if desired). There are a number of tests we want our security manager to reject if they are attempted directly by an untrusted class, but should succeed if they are attempted indirectly by an untrusted class. For these tests in Java 1.1, we have to rely on the class depth to tell us whether the call originated from an untrusted class or not. We use this method to help us with that task: Class Definitionprivate void checkClassDepth(int depth, String ask, String ex) { int clDepth = classLoaderDepth(); if (clDepth > 0 && clDepth <= depth + 1) { throw new SecurityException(ex); } } Note that we have to add 1 to the class depth for this method to succeed, since calling this method has pushed another method frame onto the stack. 6.3.2. Implementing Network AccessRegardless of the release on which your security manager is based, you typically must write the necessary methods to handle network access, because the default methods of the security manager are usually inadequate. In 1.1, the default behavior for the checkConnect() method is to throw a security violation. In 1.2, the default behavior for the checkConnect() method is to use the access controller to see if the appropriate entry is in the policy file. This is very useful in some circumstances: we can, for example, specify that all code loaded from network.xyz.com can access any other machine in the xyz.com domain, but no other machines. But we cannot set up a general rule for the mode of network access we're most accustomed to. We cannot set up a rule saying that code loaded from a particular machine can only make a network connection back to that machine. The problem lies in the fact that we cannot pattern match entries in the policy file; we cannot say something like: Class Definitiongrant codeBase http://%template/ { permission java.net.SocketPermission "%template", "connect"; }; So if we want to implement a security policy where code can only make a connection back to the host from which it was loaded, we must provide a new implementation of the checkConnect() method: Class Definitionprivate ClassLoader getNonSystemClassLoader() { Class c[] = getClassContext(); ClassLoader sys = ClassLoader.getSystemClassLoader(); for (int i = 1; i < c.length; i++) { ClassLoader cl = c[i].getClassLoader(); if (cl != null && !cl.equals(sys)) return cl: } return null } public void checkConnect(String host, int port) { try { super.checkConnect(host, port); return; } catch (AccessControlException ace) { // continue } //In 1.1, use currentClassLoader() instead ClassLoader loader = getNonSystemClassLoader(); String remoteHost; if (loader == null) return; if (!(loader instanceof JavaRunnerLoader)) throw new SecurityException("Class loader out of sync"); JavaRunnerLoader cl = (JavaRunnerLoader) loader; remoteHost = cl.getHost(); if (host.equals(remoteHost)) return; try { class testHost implements PrivilegedExceptionAction { String local, remote; testHost(String local, String remote) { this.local = local; this.remote = remote; } public Object run() throws UnknownHostException { InetAddress hostAddr = InetAddress.getByname(local); InetAddress remoteAddr = InetAddress.getByName(remote); if (hostAddr.equals(remoteAddr)) return new Boolean("true"); return new Boolean("false"); } } testHost th = new testHost(host, remoteHost); Boolean b = (Boolean) AccessController.doPrivileged(th); if (b.booleanValue()) return; } catch (PrivilegedActionException pae) { //Must be an UnknownHostException; continue and throw exception } throw new SecurityException( "Can't connect from " + remoteHost + " to " + host); } First, we check our superclass to see if it allows the connection. This is only appropriate for 1.2-based security managers--calling the superclass checks the policy file to see if the connection should be made according to information in that file. If that's true, then we simply want to return: the check should succeed. Otherwise, we continue so we can make sure the destination machine is the same machine we loaded this particular class from. For 1.1, this test must be omitted; the superclass in 1.1 would immediately throw an exception. If there is no class loader on the stack, we want to permit access to any host, so we simply return. Otherwise, we obtain the hostname the untrusted class was loaded from (via the getHost() method of the class loader) and compare that to the hostname the untrusted class is attempting to contact. If the strings are equal, we're all set and can return. Otherwise, we implement the logic we described earlier by obtaining the IP address for each hostname and comparing the two IP addresses. Note that the logic here for allowing the InetAddress class to resolve the hostname to an IP address is based on the access controller. For a 1.1-based security manager, you would set the inCheck variable to true, execute the calls that are in the run() method of the testHost class, and then set inCheck to false. You would also need to synchronize this method and the getInCheck() methods. This implementation requires yet another change to the class loader we're using. The class loader must now be able to provide us with the name of the host from which a particular class was loaded. Since our class loader is based on a URL, that's an easy method to implement: we simply return the host of the URL: Class Definitionpublic class JavaRunnerLoader extends SecureClassLoader { URL urlBase; ... other code from previous examples ... String getHost() { return urlBase.getHost(); } } If you choose to implement a different network security model for your checkConnect() method, there are a few things that you should be aware of:
You may want to implement a similar policy in the checkAccept() method so that a class can only accept a connection from the host from which it was loaded. Since we've just implemented that logic in the checkConnect() method, the easiest way to implement this method is: Class Definitionpublic void checkAccept(String host, int port) { try { super.checkAccept(host, port); return; } catch (AccessControlException ace) { // continue } checkConnect(host, port); } 6.3.3. Network Permissions in the Class LoaderIn Java 1.2, there is another way to achieve the network permissions we just outlined. Instead of overriding the checkConnect() method of the security manager, we can arrange for the protection domain of each class to carry with it the permission to open a socket to the host it was loaded from. We can add this permission without regard to the permissions that might be in the policy file. This implementation requires us to override the getPermissions() method of the SecureClassLoader class as follows: Class Definitionprotected PermissionCollection getPermissions(CodeSource cs) { if (!cs.equals(this.cs)) return null; Policy pol = Policy.getPolicy(); PermissionCollection pc = pol.getPermissions(cs); pc.add(new SocketPermission(urlBase.getHost(), "connect"); return pc; } As long as we use the correct code source to define the class, when the class loader resolves its permissions the appropriate socket permission will be added to the user-defined set of permissions. 6.3.4. Implementing Thread SecurityImplementing a model of thread security requires that you implement the checkAccess() methods as well as implementing the getThreadGroup() method. In 1.1, the checkAccess() methods by default throw a security exception. In 1.2, the default behavior of the security manager is to allow the checkAccess() method to succeed unless the target thread is a member of the system thread group or the target thread group is the system thread group. In those cases, the program must have been granted a runtime permission of modifyThread or modifyThreadGroup (depending on which checkAccess() method is involved) for the operation to succeed. Hence, any thread can modify any thread or thread group except for those belonging to the system thread group. Both releases return the thread group of the calling thread for the getThreadGroup() method. We'll show an example that implements a hierarchical notion of thread permissions which fits well within the notion of the virtual machine's thread hierarchy (see Figure 6-1). In this model, a thread can manipulate any thread that appears within its thread group or within a thread group that is descended from its thread group. In the example, Program #1 has created two thread groups. The Calc thread can manipulate itself, the I/O thread, and any thread in the Program #1 thread groups; it cannot manipulate any threads in the system thread group or in Program #2's thread group. Similarly, threads within Program #1's thread subgroup #1 can only manipulate threads within that group. Figure 6-1. A Java thread hierarchyThis is a different security model than that which is implemented by the JDK's appletviewer and by some browsers in 1.1. In those models, any thread in any thread group of the applet can modify any other thread in any other thread group of the applet, but threads in one applet are still prevented from modifying threads in another applet or from modifying the system threads. But the model we'll describe fits the thread hierarchy a little better. Note that this security model doesn't fit well within the idea of thread permissions and protection domains. An entry in the policy file granting permission to manipulate threads to the classes from which Program #1 is loaded will thus grant Program #1 permission to manipulate any threads in the virtual machine. The 1.2 default security manager checks for the modifyThread and modifyThreadGroup permissions as described above. The key to our model of thread security depends on the getThreadGroup() method. We can use this method to ensure that each class loader creates its threads in a new thread group as follows:
The simplest way to implement getThreadGroup() is to create a new thread group for each instance of a class loader. In a browser-type program, this does not necessarily create a new thread group for each applet, because the same instance of a class loader might load two or more different applets if those applets share the same codebase. If we adopt this approach, those different applets will share the same default thread group. This might be considered a feature. It is also the approach we'll show; the necessary code to put different programs loaded by the same class loader into different thread groups is a straightforward extension. Our getThreadGroup() method, then, looks like this: Class Definitionpublic ThreadGroup getThreadGroup() { ClassLoader loader = currentClassLoader(); if (loader == null || !(loader instanceof JavaRunnerLoader)) return super.getThreadGroup(); JavaRunnerLoader cl = (JavaRunnerLoader) loader; return cl.getThreadGroup(); } We want each instance of a class loader to provide a different thread group. The simplest way to implement this logic is to defer to the class loader to provide the thread group. If there is no class loader, we'll use the thread group our superclass recommends (which, if we've directly extended the SecurityManager class, will be the thread group of the calling thread). Of course, not every class loader has a getThreadGroup() method, so if the class loader we find isn't of the class that we expect, we again have to defer to our superclass to provide the correct thread group (which, by default, is the thread group of the calling thread). Otherwise, we can ask the class loader, which implies that we need to provide a getThreadGroup() method within that class loader: Class Definitionpublic class JavaRunnerLoader extends SecureClassLoader { private ThreadGroup threadGroup; private static int groupNum; ... ThreadGroup getThreadGroup() { if (threadGroup == null) threadGroup = new ThreadGroup("JavaRunner ThreadGroup-" + groupNum++); return threadGroup; } } Now we've achieved the first part of our goal: when the program attempts to create a thread without specifying a thread group that it should belong to, the thread is assigned to the desired group. For the second part of our goal, we need to ensure that the checkAccess() method only allows classes from that class loader to create a thread within that thread group (or one of its descendent thread groups). In order to achieve this second goal, we must implement the checkAccess() methods as follows: Class Definitionpublic void checkAccess(Thread t) { ThreadGroup current = Thread.currentThread().getThreadGroup(); if (!current.parentOf(t.getThreadGroup())) super.checkAccess(t); } public void checkAccess(ThreadGroup tg) { ThreadGroup current = Thread.currentThread().getThreadGroup(); if (!current.parentOf(tg)) super.checkAccess(tg); } This logic prevents threads in sibling thread groups from manipulating each other, as well as preventing threads in groups that are lower in the thread hierarchy from manipulating threads in their parent groups. Though that makes it more restrictive than the model employed by the 1.1 JDK, it matches the concept of a thread group hierarchy better than the JDK's model. There are three caveats with this model. The first has to do with the way in which thread groups are created. When you create a thread group without specifying a parent thread group, the new thread group is placed into the thread hierarchy as a child of the thread group of the currently executing thread. For example, in Figure 6-1, when the Calc thread creates a new thread group, by default that thread group is a child of Program Thread Group #1 (e.g., it could be Program Subgroup #1). Hence, if you start a program, you must ensure that it starts by executing it in the thread group that would be returned by its class loader--that is, the default thread group of the program. That's why we included that logic at the beginning of our JavaRunner example. The second caveat is that threads may not be expecting this type of enforcement of the thread hierarchy, since it does not match many popular browser implementations. Hence, programs may fail under this model, while they may succeed under a different model. Finally, remember that in 1.2, the stop() method of the Thread class first calls the checkPermission() class of the security manager to see if the current stack has a runtime permission of "stopThread". For backward compatibility, all protection domains have that permission by default, but a particular user may change that in the policy file. 6.3.5. Implementing Package AccessA final area for which the default security manager is sometimes inadequate is the manner in which it checks for package access and definition. In 1.1, the default security manager rejects all package access and definition attempts. In 1.2, the situation is complex. For package access, the security manager looks for a property defined in the java.security file named package.access. This property is a list of comma-separated package names for which access should be checked. If the class loader uses the checkPackageAccess() method (many do not) and attempts to access a package in the list specified in the java.security file, then the program must have a runtime permission with a name of accessClassInPackage.<packagename>. For defining a class, the operation is similar; the property name in the java.security file is package.definition, and the appropriate runtime permission has a name of defineClassInPackage.<packagename>. This model works well, but it requires that the java.security file and all the java.policy files be coordinated in their attempts to protect package access and definition. For that reason, and also to provide a better migration between releases (and because it's the only way to do it in 1.1), you may want to include the logic to process some policies within your new security manager. In that way, users will not need to make any changes on their system; in this case, the user will not have to put the appropriate RuntimePermission entries into the java.policy files by hand. The checkPackageAccess() method is most often used to restrict untrusted classes from directly calling certain packages--e.g., you may not want untrusted classes directly calling the com.xyz.support pacakge of your application. Unfortunately, the only way to do that while relying on the security manager is to rely on the class depth, which we want to avoid. One solution is to introduce a property for the application that defines packages that the untrusted classes in the application are not allowed to access. HotJava and the appletviewer do this by setting properties of the form: Class Definitionpackage.restrict.access.pkgname = true In the checkPackageAccess() method, you can use the parameter to construct this property (substituting for the pkgname) and see if the corresponding property is set: if it is, and if the inClassLoader() method returns true, you can throw the security exception. For our purposes, however, we will allow classes to access any package, and write our checkPackageAccess() method like this: Class Definitionpublic void checkPackageAccess(String pkg) { } The checkPackageDefinition() method is somewhat different--you probably don't want untrusted classes defining things in the java package, for example. So we want to test for that package explicitly. But we also want to respect the permissions for the applications, so the general solution for cases such as this is to first check with the access controller (via the security manager's superclass), and then to implement the original logic: Class Definitionpublic void checkPackageDefinition(String pkg) { if (!pkg.startsWith("java.")) return; try { super.checkPackageDefinition(pkg); return; } catch (AccessControlException ace) { // continue } if (inClassLoader()) throw new SecurityException("Can't define java classes"); } Note that the name in the test contains the period separator--you don't want an untrusted class to be able to define a class named java.lang.String, but you do want it to be able to define a class named javatest.myClass. On the other hand, you may or may not want to grant access to classes in the javax package. This method also requires a change to the class loader that we'll show at the end of the chapter. 6.3.6. Establishing a Security Policy in 1.2We'll now give specific information on how to establish a security policy for 1.2. In Java 1.2, the SecurityManager class is a concrete class--you use it directly, or you may subclass it. The simplest implementation of the SecurityManager class is: Class Definitionpublic class JavaRunnerManager extends SecurityManager { } The JavaRunnerManager class inherits the default behavior of the SecurityManager class for all its methods--but it's important to realize that this default behavior is not the behavior we discussed in Chapter 4, "The Security Manager Class". The behavior we discussed in that chapter stemmed from the security manager implementations of various popular browsers--that may be the security that is appropriate for your application, but the default behavior for the Security Manager class comes from the java.policy files. The default behavior of the public methods of the SecurityManager class is to call the access controller with an appropriate permission. For example, the implementation of the checkExit() method is: Class Definitionpublic void checkExit(int status) { AccessController.checkPermission(new RuntimePermission("exitVM")); } This is why the default security policy for the application can be specified via the java.policyfiles. Table 6-3 lists the methods of the security manager and the permission they construct when they call the access controller. Table 6-3. The Relationship Between the Security Manager and the Access ControllerThere are five slight exceptions to the rules laid out in Table 6-3:
For the most part, it's possible to use the default security manager and the permission mappings we've just identified to support virtually any security policy. But there are certain useful exceptions a security manager will often define:
For a complete 1.2-based security manager, then, you typically need to override only the methods involved with these four exceptions. The 1.2-based security manager we'll use for our JavaRunner program looks like this: Class Definitionpublic class JavaRunnerManager extends SecurityManager { public void checkConnect(String host, int port) { .. follow implementation given above .. } public void checkPackageAccess(String pkg) { .. follow implementation given above .. } public void checkPackageDefinition(String pkg) { .. follow implementation given above .. } public void checkExit(int status) { } public void checkAccess(Thread t) { .. follow implementation given above .. } public void checkAccess(ThreadGroup tg) { .. follow implementation given above .. } } 6.3.7. Establishing a 1.1 Security PolicyEstablishing a security policy in 1.1 is done only by ensuring that the correct security manager is in place. In this section, we're going to discuss how a 1.1-based security manager can be implemented. 6.3.7.1. The RMI security managerOne of the times a security manager is often used in a Java application is in an RMI server. An RMI server has the capability of loading code from an RMI client located on a remote machine and executing that code on the server--essentially transforming the server (temporarily) into a client.[5] In essence, the security ramifications of using RMI servers are similar to those of an applet, but in reverse: you now want to protect your server machine from the side effects of untrusted code it got from a client.
In the most common case, you'll want your RMI server to have a simple security model. If the code it's executing was completely loaded from the server, the operation should succeed; if any of the code it's executing was loaded from the client, the operation should fail. Hence, the Java API provides the RMISecurityManager class, which implements just such a policy. In general, the methods of the RMISecurityManager class look like this: Class Definitionpublic void checkAccess(Thread t) { if (inClassLoader()) throw new SecurityException("checkAccess"); } You can check the source code (java.rmi.RMISecurityManager) for exact details; this example is a conflation of code found there. Hence, in the RMI security manager, all local code is trusted and all remote code is untrusted. There are certain methods of this class that have slightly different implementations, however. Because the RMISecurityManager provides a useful basis for a default implementation of your own security manager, we'll list those exceptions here so you can use the RMISecurityManager class and understand where you're starting out.
6.3.7.2. A complete 1.1 security managerIn Java 1.1, the SecurityManager class is abstract, so you can't directly instantiate a security manager object. However, none of the methods of SecurityManager is itself abstract, meaning that the simplest implementation of the SecurityManager class is this: Class Definitionpublic class StrictSecurityManager extends SecurityManager { } The StrictSecurityManager class inherits the default behavior of the SecurityManager class for all its methods--but once again it's important to realize that this default behavior is not the behavior we discussed earlier in terms of what an untrusted class might or might not be allowed to do. The default behavior of the public methods in the SecurityManager class in 1.1--and hence of the StrictSecurityManager class above--is to deny every operation to every class, trusted or not. Each of the public methods of the SecurityManager class looks similar to this: Class Definitionpublic void checkAccess(Thread g) { throw new SecurityException (); } Thus, if you want to implement your own security manager, you need only override the methods for which you want to provide a more relaxed security policy. If you want to allow (at least some) thread operations, you must override the checkAccess() methods; if you do not override those methods, no thread operations will be allowed by any class. In typical usage, a 1.1-based security manager might want to deny a large number of operations if there is any untrusted class on the stack. These methods might be implemented with the checkClassLoader() method we discussed above. Candidates for this type of check are:
Similarly, there are a number of tests that we want to fail if they are attempted directly by an untrusted class, but that we want to succeed if they are attempted indirectly by an untrusted class. For these tests, we have to rely on the class depth to tell us whether the call originated from an untrusted class or not; we use the checkClassDepth() method to help us with that task. Here are the candidate methods for this test along with the depth that checked for each method:
Finally, there are some methods we must implement with their own logic. Although we've saved these for last, they are most interesting since these are the methods that you'll need to pay the most attention to when you write your own security manager. 6.3.7.3. Implementing the file access methodsIf you are going to implement a security manager, you must determine a policy for reading and writing files and implement it in each of the checkRead() and checkWrite() methods. The logic you put into each method is slightly different. In the case where these methods take a single string argument, the logic is straightforward: the program is attempting to open a file with the given name, and you should either accept or reject that operation. We'll base our decision on the depth of the class loader. Untrusted classes may not directly open a file for reading or writing, but they may cause that to happen through the Java API: Class Definitionpublic void checkRead(String file) { checkClassDepth(2, "Read the file " + file, "Can't read local files"); } public void checkWrite(String file) { checkClassDepth(2, "Write the file " + file, "Can't write local files"); } In the case where these methods take a FileDescriptor as an argument, the policy is a little harder to define. As far as the Java API is concerned, these methods are only called as a result of calling the Socket.getInputStream() or Socket.getOutputStream() methods--which means that the security manager is really being asked to determine if the socket associated with the given file descriptor should be allowed to be read or written. By this time, the socket has already been created and has made a valid connection to the remote machine, and the security manager has had the opportunity to prohibit that connection at that time. What type of access, then, would you prohibit when you implement these methods? It partially depends on the types of checks your security manager made when the socket was created. We'll assume for now that a socket created by an untrusted class can only connect to the site from which the class was loaded, while a socket created by a trusted class can connect to any site. Hence, you might want to prohibit an untrusted class from opening the data stream of a socket created by a trusted class--although if the class is trusted, you typically want to trust that class's judgement, and if that class passed the socket reference to an untrusted class, the untrusted class should be able to read from or write to the socket. On the other hand, it is important to be sure that these methods are actually being called from the socket class. An untrusted class could attempt to pass an arbitrary file descriptor to the File*Stream constructor, breaking into your machine. Typically, then, the only checks you put into this method are to determine that the FileDescriptor object is valid and the FileDescriptor object does indeed belong to the socket class: Class Definitionpublic void checkRead(FileDescriptor fd) { if (!inClassLoader()) return; if (!fd.valid() || !inClass("java.net.SocketInputStream")) throw new SecurityException("Can't read a file descriptor"); } public void checkWrite(FileDescriptor fd) { if (!inClassLoader()) return; if (!fd.valid() || !inClass("java.net.SocketOutputStream")) throw new SecurityException("Can't write a file descriptor"); } 6.3.7.4. Implementing network, thread, and package accessA typical 1.1-based security manager would implement thread, network, and package access as we described above. 6.3.7.5. Implementing miscellaneous methodsThere is one more method of the security manager that we must implement with slightly different rules: the checkTopLevelWindow() method. This method uses the standard class depth test for an untrusted class, but it shouldn't throw an exception, so it looks like this: Class Definitionpublic boolean checkTopLevelWindow(Object window) { if (classLoaderDepth() == 3) return false; return true; } Copyright © 2001 O'Reilly & Associates. All rights reserved. | ||||||||||||||||||||||||
|