Protected Methods of the Security Manager
Security Managers and the Class Loader
Implementation Techniques
Running Secure Applications
Summary
In Chapter 4, "The Security Manager Class", we examined the security manager in the context of existing implementations of the security manager for use in Java-enabled browsers; we followed that with a discussion of the access control mechanism and Java's ability to define access policies.
In this chapter, we'll put that information together and look at how the security manager is actually implemented, and how you can implement your own security manager. There are three times when it's important to write your own security manager:
RMI wants you to provide a security manager for all RMI servers; for RMI servers that load client classes, a security manager is required. There is a default RMI security manager that you may use for this purpose, or you may write your own.
If you're writing your own Java-enabled browser, you'll want to provide a security manager. In addition, if you're using an existing browser, you may want to use a different security manager in that browser. Some browsers already allow the user to specify a different security manager via a property; other browsers can be licensed for this type of customization.
If you download, install, and run Java applications on your machine, you may want to provide a security manager to protect your system against those applications the same way that it is protected against Java applets. In Java 1.1 and earlier releases, this requires you to write a security manager. In Java 1.2,[1] you can use the access control mechanism instead of writing a complete security manager. However, even in Java 1.2 you may need to write your own security manager in certain circumstances. There are methods (like the getThreadGroup() method) of the security manager that are outside the scope of the access controller, and there are certain types of permissions (like those typically given to the checkConnect() method) that cannot be specified in a java.policy file.
[1]1.2 is now Java 2.
We've often said that the distinction between trusted and untrusted code has its roots in information that the security manager must obtain from the class loader. There are two ways in which this happens: through a set of generic methods of the SecurityManager class that inform the security manager about the state of the class loader, and through an agreed-upon interface between the security manager and the class loader. We'll look at the first of these mechanisms in this section, and we'll discuss the second mechanism later when we actually develop a security manager.
The use of these protected methods is vital in Java 1.1 and previous releases. In Java 1.2, they are much less important--some of them have even been deprecated. This is not surprising, since the access controller now gives us much of the information that initially could only be obtained from the class loader. We'll give a complete overview of these methods here, although it is information that you'll only need to complete a 1.1-based security manager.
The methods of the security manager that provide us with generic information about the class loader are all protected methods of the security manager class; they are summarized in Table 6-1.
Method |
Purpose |
---|---|
Class DefinitiongetClassContext() |
Return all the classes on the stack to see who has called us |
Class DefinitioncurrentClassLoader() |
Return the most recent class loader |
Class DefinitioncurrentLoadedClass() |
Return the class that was most recently loaded with a class loader |
Class DefinitionclassLoaderDepth() |
Return the depth in the call stack where the most recent class loader was found |
Class DefinitionclassDepth() |
Return the depth in the call stack of the given class |
Class DefinitioninClass() |
Return true if the given class is on the stack |
Class DefinitioninClassLoader() |
Return true if any class on the stack came from a class loader |
Return an array of all classes on the stack of the currently executing thread.
The first such method we'll discuss lets us retrieve all the classes involved in making the current call to the security manager. This method itself is rarely used in a security manager, but it is the basis for many of the methods we'll discuss in this section.
The getClassContext() method returns an array of Class objects in the order of the call stack for the current method. The first element of the array is always the Class object for the security manager class, the second element is the Class object for the method that called the security manager, and so on.
Accessing all the classes in this array is one way to determine whether the call originally came from code that is in the Java API or whether it came from other code. For example, we could put the following method into our custom security manager:
public class MySecurityManager extends SecurityManager { public void checkRead(String s) { Class c[] = getClassContext(); for (int i = 0; i < c.length; i++) { String name = c.getName(); System.out.println(name); } } }
If we then try to create a FileReader object:
public class Test { public static void main(String args[]) { FileReader f = new FileReader("/etc/passwd"); } }
we see the following output from the checkRead() method:
MySecurityManager java.io.FileInputStream java.io.FileReader Test
In other words, a method in the Test class invoked a method in the FileReader class, which invoked a method in the File InputStream class, which invoked a method (the checkRead() method, in fact) in the MySecurityManager class.
The policies you want to enforce determine how you use the information about these classes--just keep in mind that the first class you see is always your security manager class and the second class you see is normally some class of the Java API. This last case is not an absolute--it's perfectly legal, though rare, for any arbitrary class to call the security manager. And as we saw in Chapter 4, "The Security Manager Class", some methods are called by platform-specific classes that implement particular interfaces of the Java API (such as methods that implement the Toolkit class).
Also keep in mind that there may be several classes from the Java API returned in the class array--for example, when you construct a new thread, the Thread class calls the checkAccess() method; the classes returned from the getClassContext() method in that case are:
MySecurityManager java.lang.Thread java.lang.Thread java.lang.Thread java.lang.Thread Test Test
We get this output because the Thread class constructor calls three other internal methods before it calls the security manager. Our Test class has created a thread in an internal method as well, so the Test class also appears twice in the class array.
Search the array of classes returned from the getClassContext() method for the most recently called class that was loaded via a program-defined class loader, and return that class loader.
The objects in the class array returned from the getClassContext() method are generally used to inspect the class loader for each class--that's how the security manager can make a policy decision about classes that were loaded from disk versus classes that were loaded from the network (or elsewhere). The simplest test that we can make is to see if any of the classes involved in the current method invocation are loaded from the network, in which case we can deny the attempted operation. This is the method we use to do that.
To understand currentClassLoader(), we need to recall how the class loader works. The class loader first calls the findSystemClass() method, which attempts to find the class in the user's CLASSPATH (or system classpath in 1.2). If that call is unsuccessful, the class loader loads the class in a different manner (e.g., by loading the class over the network). As far as the Java virtual machine is concerned, the class loader associated with a class that was loaded via the findSystemClass() method is null. If an instance of the ClassLoader class defined the class (by calling the defineClass() method), then (and only then) does Java make an association between the class and the class loader. This association is made by storing a reference to the class loader within the class object itself; the getClassLoader() method of the Class object can be used to retrieve that reference.
Hence, the currentClassLoader() method is equivalent to:[2]
[2]The truth is that the currentClassLoader() method is written in native code, so we don't know how it actually is implemented, but it is functionally equivalent to the code shown. This is true about most of the methods of this section, which for efficiency reasons are written in native code.
protected ClassLoader currentClassLoader () { Class c[] = getClassContext(); for (int i = 1; i < c.length; i++) if (c[i].getClassLoader () !=null) return c[i].getClassLoader (); return null; }
We can use this method to disallow writing to a file by any class that was loaded via a class loader:
public void checkWrite(String s) { if (currentClassLoader() !=null) throw new SecurityException("checkWrite"); } }
With this version of checkWrite(), only the Java virtual machine can open a file for writing. When the Java virtual machine initializes, for example, it may create a thread for playing audio files. This thread will attempt to open the audio device on the machine by instantiating one of the standard Java API file classes. When the instance of this class is created, it (as expected) calls the checkWrite() method, but there is no class loader on the stack. The only methods that are involved in the thread opening the audio device are methods that were loaded by the Java virtual machine itself and hence have no class loader. Later, however, if an applet class tries to open up a file on the user's machine, the checkWrite() method is called again, and this time there is a class loader on the stack: the class loader that was used to load in the applet making the call to open the file. This second case will generate the security exception.
A number of convenience methods of the security manager class also relate to the current class loader:
Test to see if there is a class loader on the stack:
protected boolean inClassLoader() { return currentClassLoader() != null; }
Return the class on the stack that is associated with the current class loader:
protected Class currentLoadedClass() { Class c[] = getClassContext(); for (int i = 0; i < c.length; i++) if (c[i].getClassLoader() != null) return c[i]; return null; }
Return the index of the class array from the getClassContext() method where the named class is found:
protected int classDepth(String name) { Class c[] = getClassContext(); for (int i = 0; i < c.length; i++) if (c[i].getName().equals(name)) return i; return -1; }
Indicate whether the named class is anywhere on the stack:
protected boolean inClass (String name) { return classDepth (name) >= 0; }
Many of these convenience methods revolve around the idea that an untrusted class may have called a method of a trusted class and that the trusted class should not be allowed to perform an operation that the untrusted class could not have performed directly. These methods allow you to write a Java application made up of trusted classes that itself downloads and runs untrusted classes. The HotJava browser is the best-known example of this sort of program. For example, the security manager of the HotJava browser does not allow an arbitrary applet to initiate a print job, but HotJava itself can.
HotJava initiates a print job when the user selects the "Print" item from one of the standard menus. Since the request comes from a class belonging to the HotJava application itself (that is, the callback method of the menu item), the browser is initiating the request (at least as far as the security manager is concerned). An applet initiates the request when it tries to create a print job.
In both cases, the getPrintJob() method of the Toolkit class calls the checkPrintJobAccess() method of the security manager. The security manager must then look at the classes on the stack and determine if the operation should succeed. If there is an untrusted (applet) class anywhere on the stack, the print request started with that class and should be rejected; otherwise, the print request originated from the HotJava classes and is allowed to proceed.
Note the similarity between this technique and the manner in which the access controller works. In Java 1.2, the HotJava classes belong to the system domain, so they are allowed to do anything; the classes that make up the applet, however, are prohibited from initiating the print job (unless, of course, an entry that enables printing for that applet's code source is in the policy file). This is why these methods have been deprecated in 1.2, where the access controller is the desired mechanism to provide this functionality.
The example that we just gave is typical of the majority of security checks the security manager makes. You can often make a decision on whether or not an operation should be allowed simply by knowing whether or not there is a class loader on the stack, since the presence of a class loader means that an untrusted class has initiated the operation in question.
There's a group of tricky exceptions to this rule, however, and those exceptions mean that you sometimes have to know the exact depth at which the class loader was found. Before we dive into those exceptions, we must emphasize: the use of the class loader depth is not pretty. Fortunately, beginning with Java 1.2, this method has been deprecated, and we need no longer concern ourselves with it. If you need to write a 1.1-compatible security manager, however, you need to use the information in this section.
The depth at which the class loader was found in the class context array can be determined by this method:
Return the index of the class array from the getClassContext() method where the current class loader is found:
protected int classLoaderDepth() { Class c[] = getClassContext(); for (int i = 0; i < c.length; i++) { if (c[i].getClassLoader() != null) return i; } return -1; }
Let's look at this method in the context of the following applet:
public class DepthTest extends Applet { native void evilInterface(); public void init() { doMath(); infiltrate(); } public void infiltrate() { try { System.loadLibrary("evilLibrary"); evilInterface(); } catch (Exception e) {} } public void doMath() { BigInteger bi = new BigInteger("100"); bi = bi.add(new BigInteger("100")); System.out.println("answer is " + bi); } }
Under normal circumstances, we would expect the doMath() method to inform us (rather inefficiently) that 100 plus 100 is 200. We would further expect the call to the infiltrate() method to generate a security exception, since an untrusted class is not normally allowed to link in a native library.
The security exception in this case is generated by the checkLink() method of the security manager. When the infiltrate() method calls the System.loadLibrary() method, the loadLibrary() method in turn calls the checkLink() method. If we were to retrieve the array of classes (via the getClassContext() method) that led to the call to the checkLink() method, we'd see the following classes on the stack:
MySecurityManager (the checkLink() method) java.lang.Runtime (the loadLibrary() method) java.lang.System (the loadLibrary() method) DepthTest (the infiltrate() method) DepthTest (the init() method) ... other classes from the browser ...
Because the untrusted class DepthTest appears on the stack, we are tempted to reject the operation and throw a security exception.
Life is not quite that simple in this case. As it turns out, the BigInteger class contains its own native methods and hence depends on a platform-specific library to perform many of its operations. When the BigInteger class is loaded, its static initializer attempts to load the math library (by calling the System.loadLibrary() method), which is the library that contains the code to perform these native methods.
Because of the way in which Java loads classes, the BigInteger class is not loaded until it is actually needed--that is, until the doMath() method of the DepthTest class is called. If you recall our discussion from Chapter 3, "Java Class Loaders" regarding how the class loader works, you'll remember that when the doMath() method is called and needs access to the BigInteger class, the class loader that created the DepthTest class is asked to find that class (even though the BigInteger class is part of the Java API itself). Hence, the applet class loader (that is, the class loader that loaded the DepthTest class) is used to find the BigInteger class, which it does by calling the findSystemClass() method. When the findSystemClass() method loads the BigInteger class from disk, it runs the static initializers for that class, which call the System.loadLibrary() method to load in the math library.
The upshot of all this is that the System.loadLibrary() method calls the security manager to see if the program in question is allowed to link in the math library. This time, when the checkLink() method is called, the class array from the getClassContext() method looks like this:
MySecurityManager (the checkLink() method) java.lang.Runtime (the loadLibrary() method) java.lang.System (the loadLibrary() method) java.math.BigInteger (the static intializer) java.lang.ClassLoader (the findSystemClass() method) AppletLoader (the loadClass() method) java.lang.ClassLoader (the loadClassInternal() method) DepthTest (the doMath() method) DepthTest (the init() method) ... various browser classes ...
As we would expect, the first three elements of this list are the same as the first three elements of the previous list--but after that, we see a radical difference in the list of classes on the stack. In both cases, the untrusted class (DepthTest) is on the stack, but in this second case, it is much further down the stack than it was in the first case. In this second case, the untrusted class indirectly caused the native library to be loaded; in the first case the untrusted class directly requested the native library to be loaded. That distinction is what drives the use of the classLoaderDepth() method.
So in this example, we need the checkLink() method to obtain the depth of the class loader (that is, the depth of the first untrusted class on the stack) and behave appropriately. If that depth is 3, the checkLink() method should throw an exception, but if that depth is 7, the checkLink() method should not throw an exception. There is nothing magical about a class depth of 7, however--that just happens to be the depth returned by the classLoaderDepth() method in our second example. A different example might well have produced a different number, depending on the classes involved.
There is, however, something special about a class depth of 3 in this example: a class depth of 3 always means that the untrusted class called the System.loadLibrary() method, which called the Runtime.loadLibrary() method, which called the security manager's checkLink() method.[3] Hence, when there is a class depth of 3, it means that the untrusted class has directly attempted to load the library. When the class depth is greater than 3, the untrusted class has indirectly caused the library to be loaded. When the class depth is 2, the untrusted class has directly called the Runtime.loadLibrary() method--which is to say again that the untrusted class has directly attempted to load the library. When there is a class depth of 1, the untrusted class has directly called the checkLink() method--which is possible, but that is a meaningless operation. So in this case, a class depth that is 3 or less (but greater than -1, which means that no untrusted class is on the stack) indicates that the call came directly from an untrusted class and should be handled appropriately (usually meaning that a security exception should be thrown).
[3]Theoretically, it could also mean that an untrusted class has called a trusted class that has called the Runtime.loadLibrary() method directly. However, the Java API never bypasses the System.loadLibrary() method, so that will not happen in practice. If you expect trusted classes in your Java application to work under the scenario we're discussing here, you must also follow that rule.
But while 3 is a magic number for the checkLink() method, it is not necessarily a magic number for all other methods. In general, for most methods the magic number that indicates that an untrusted class directly attempted an operation is 2: the untrusted class calls the Java API, which calls the security manager. Other classes have other constraints on them that change what their target number should be.
The class depth is therefore a tricky thing: there is no general rule about the allowable class depth for an untrusted class. Worse, there's no assurance that the allowable class depth may not change between releases of the JDK--the JDK could conceivably change its internal algorithm for a particular operation to add another method call, which would increase the allowable class depth by 1. This is one reason why the class depth is such a bad idea: it requires an intimate knowledge of all the trusted classes in the API in order to pick an appropriate class depth. Worse, a developer may introduce a new method into a call stack and completely change the class depth for a sensitive operation without realizing the effect this will have on the security manager.
Nonetheless, in order for certain classes of the Java API to work correctly, you need to put the correct information into your 1.1-based security manager (such as in the checkLink() method that we just examined). The methods that need such treatment are summarized in Table 6-2.
In all cases in Table 6-2, the Java API depends on being allowed to perform an operation that would normally be rejected if an untrusted class performed it directly. The JavaBeans classes, for example, create a class loader (an instance of SystemClassLoader) in order to abstract the primordial class loader. So if an untrusted class creates a Java bean, that Java bean must in turn be allowed to create a class loader, or the bean itself won't work.
Note that not every target depth in this table is 2. In the case of the Thread and ThreadGroup classes, operations that affect the state of the thread call the checkAccess() method of the Thread or ThreadGroup class itself, which in turn calls the checkAccess() method of the SecurityManager class. This extra method call results in an extra method on the stack and effectively increases the target depth by 1. Similarly, the checkTopLevelWindow() method is called from the constructor of the Window class, which in turn is called from the constructor of the Frame class, resulting in a target depth of 3.
Remember that this table only summarizes the methods of the security manager where the actual depth of the class loader matters to the core Java API. If you're writing your own application, you need to consider whether or not your application classes want to perform certain operations. If you want classes in your application to be able to initiate a print job, for example, and you don't want untrusted classes that your application loads to initiate a print job, you'll want to put a depth check of 2 into the checkPrintJobAccess() method. In general, for methods that aren't listed in the above table, a depth of 2 is appropriate if you want your application classes (i.e., classes from the CLASSPATH) to be able to perform those operations.
There is once again a nice similarity between these ideas and the access controller. When you call the doPrivileged() method of the access controller, you're achieving the same thing a security manager achieves by testing the class depth. The point to remember about the class depth is that it allows the security manager to grant more permissions to a class than it would normally have--just like the doPrivileged() method grants its permissions to all protection domains that have previously been pushed onto the stack. Of course, the access controller is a much smarter way to go about this, since it doesn't depend upon someone getting the class depth right; it only depends upon the actual characteristics of the stack during execution of the program.
There is a single protected instance variable in the security manager class, and that is the inCheck instance variable:
The value of this variable can be obtained from the following method:
Return the value of the inCheck instance variable.
Since there is no corresponding public method to set this variable, it is up to the security manager itself to set inCheck appropriately.[4]
[4]Don't get all excited and think that your untrusted class can use this method to see when the security manager is working. As we'll see, it's only set by the security manager in a rare case, and even if it were set consistently, there's no practical way for your untrusted class to examine the variable during the short period of time it is set.
This variable has a single use: it must be set by the security manager before the security manager calls most methods of the InetAddress class. The reason for this is to prevent an infinite recursion between the security manager and the InetAddress class. This recursion is possible under the following circumstances.
An untrusted class attempts to open a socket to a particular host (e.g., sun.com). The expectation is that if the untrusted class was loaded from sun.com that the operation will succeed; otherwise, the operation will fail.
Opening the socket results in a call to the checkConnect() method, which must determine if the two hosts in question are the same. In the case of a class loaded from sun.com that is attempting to connect to sun.com, a simple string comparison is sufficient. If the names are the same, the checkConnect() method can simply return immediately. In fact, this is the only logic performed by some browsers--if the names do not literally match, the operation is denied immediately.
A complication arises if the two names do not match directly, but may still be the same host. My machine has a fully qualified name of piccolo.East.Sun.COM; browsers on my local area network can access my machine's web server as piccolo, piccolo.East, or piccolo.East.Sun.COM. If the untrusted class is loaded from a URL that contained only the string piccolo, and the class attempts to open a socket to piccolo.East, we may want that operation to succeed even though the names of the hosts are not equal.
Hence, the checkConnect() method must retrieve the IP address for both names, and compare those IP addresses.
To retrieve the IP address for a particular host, the checkConnect() method must call the InetAddress.getByName() method, which converts a string to an IP address.
The getByName() method will not blithely convert a hostname to an IP address--it will only do so if the program in question is normally allowed to make a socket connection to that host. Otherwise, an untrusted class could be downloaded into your corporate network and determine all the IP addresses that are available on the network behind your firewall. So the getByName() method needs to call the checkConnect() method in order to ensure that the program is allowed to retrieve the information that is being requested.
We see the problem here: the getByName() method keeps calling the checkConnect() method, which in turn keeps calling the getByName() method. In order to prevent this recursion, the checkConnect() method is responsible for setting the inCheck instance variable to true before it starts and then setting it to false when it is finished. Similarly, the getByName() method is responsible for examining this variable (via the return from the getInCheck() method); it does not call the checkConnect() method if a security check is already in progress.
There may be other variations in this cooperation between the security manager and the InetAddress class--other methods of the InetAddress class also use the information from the getInCheck() method to determine whether or not to call the checkConnect() method. But this is the only class where this information is used directly. You can set the inCheck method within other methods of your security manager, but there is no point in doing so.
In 1.2, this variable and method are deprecated. The correct operation to perform in a 1.2-based security manager is to place the calls to the InetAddress in a class that can be used by the doPrivileged() method. In addition, the InetAddress class in 1.2 no longer calls the getInCheck() method.
If you implement a checkConnect() method that calls the InetAddress class and sets the inCheck variable, you must make the checkConnect() method and the getInCheck() methods synchronized. This prevents another thread from directly looking up an IP address at the same time that the security manager has told the InetAddress class not to call the checkConnect() method.
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |