Part of the security implications of a class loader depend upon its internal implementation. When you implement a class loader, you have two basic choices: you can extend the ClassLoader class, or you can extend the SecureClassLoader class. The second choice is preferred, but it is not an option for Java 1.1. If you're programming in 1.2, you may choose to use the URL class loader rather than implementing your own, but the information in this section will help you understand the security features of the URL class loader. In this section, then, we'll look at how to implement both default and secure class loaders.
Aside from the primordial class loader, all Java class loaders must extend the ClassLoaderclass (java.lang.ClassLoader). Since the ClassLoader class is abstract, it is necessary to subclass it to create a class loader.
In order to implement a class loader, we start with this method:
Using the rules of the class loader, find the class with the given name and, if indicated by the resolve variable, ensure that the class is resolved. If the class is not found, this method should throw a ClassNotFoundException. This method is abstract in 1.1, but not in 1.2. In 1.2, you typically do not override this method.
The loadClass() method is passed a fully qualified class name (e.g., java.lang.String or com.xyz.XYZPayrollApplet), and it is expected to return a class object that represents the target class. If the class is not a system class, the loadClass() method is responsible for loading the bytes that define the class (e.g., from the network).
There are five final methods (listed below) in the ClassLoader class that a class loader can use to help it achieve its task.
Create a Class object from an array of bytecodes. The defineClass() method runs the data through the bytecode verifier and then creates the Class object. This method also ensures that the name in the class file is the same as the name of the argument--that is, that the bytes actually define the desired class. We'll look at protection domains in Chapter 5, "The Access Controller"; if you use the signature without one, a default (system) domain will be provided for the class.
Attempt to find the named class by using the internal class loader to search the user's CLASSPATH. If the system class is not found, a ClassNotFoundException is generated. In 1.2, this method searches only the classes in the Java API.
Find the class object for a class previously loaded by this class loader. If the class is not found, a null reference is returned.
For a given class, resolve all the immediately needed class references for the class; this will result in recursively calling the class loader to ask it to load the referenced class.
The loadClass() method is responsible for implementing the eight steps of the class definition list given above. Typically, implementation of this method looks like this:
protected Class loadClass(String name, boolean resolve) { Class c; SecurityManager sm = System.getSecurityManager(); // Step 1 -- Check for a previously loaded class c = findLoadedClass(name); if (c != null) return c; // Step 2 -- Check to make sure that we can access this class if (sm != null) { int i = name.lastIndexOf('.'); if (i >= 0) sm.checkPackageAccess(name.substring(0, i)); } // Step 3 -- Check for system class first try { // In 1.2 only, defer to another class loader if available if (parent != null) c = parent.loadClass(name, resolve); else // Call this method in both 1.1 and 1.2 c = findSystemClass(name); if (c != null) return c; } catch (ClassNotFoundException cnfe) { // Not a system class, simply continue } // Step 4 -- Check to make sure that we can define this class if (sm != null) { int i = name.lastIndexOf('.'); if (i >= 0) sm.checkPackageDefinition(name.substring(0, i)); } // Step 5 -- Read in the class file byte data[] = lookupData(name); // Step 6 and 7 -- Define the class from the data; this also // passes the data through the bytecode verifier c = defineClass(name, data, 0, data.length); // Step 8 -- Resolve the internal references of the class if (resolve) resolveClass(c); return c; }
For most of the class loaders we're interested in, this skeleton of a class loader is sufficient, and all we need to change is the definition of the lookupData() method (as well as the constructor of the class, which might need various initialization parameters).
This method might be used to implement a 1.1-based class loader, where the loadClass() method is abstract. In 1.2, however, it is easier to use the existing loadClass() method and override only the existing findClass() method:
Load the given class according to the internal rules of the class loader. This method should assume that it is responsible for implementing only steps 5, 6, and 7 in our list: that is, it should read the data and call the defineClass() method, but it needn't look for an existing implementation of the class or check to see if it is a system class. If the class cannot be found, this method should return null (which is what the default implementation of this method returns in all cases).
We'll use this method in our example of a secure class loader. If you must implement a 1.1-based class loader, you can use the code from that example to implement a lookupData() method that could be used by the above implementation of the loadClass() method.
From a security point of view, the loadClass() method is important because it codifies several aspects of how Java handles security. One example of this is that the order in which the loadClass() method looks for classes is significant. Much of the security within Java itself depends on classes in the Java API doing the correct thing--e.g., the java.lang.String class is final and holds the array of characters representing the string in a private instance variable; this allows strings to be considered constants, which is important to several aspects of Java security. When a class loader is asked to find the java.lang.String class, it is very important that it return the class from the Java API rather than returning a class (possibly having different and insecure semantics) it loaded from a different location.
Hence, it is important that the class loader call the findSystemClass() method immediately after it attempts (and fails) to find the class in its internal cache (via the findLoadedClass() method). By codifying this behavior in the loadClass() method, the ClassLoader class ensures that the class loader will have the correct behavior to enforce the overall security of the virtual machine. This is why the loadClass() method is no longer abstract in 1.2. This method really should be made final now, but that would break compatibility with previously written class loaders.
Violating security by returning the incorrect class would have required the cooperation of the class loader. This might have happened accidentally, if the author of the class loader did not provide a correct implementation. It might also have happened maliciously, if the author of the class loader intentionally wrote an incorrect implementation. The new implementation solves the first problem, but not the second: the author of the class loader can still override the loadClass() method directly to do whatever he wants. In general, you have to trust the author of your class loader anyway, so the new implementation enhances security mostly by assisting developers in writing more robust programs.
Starting with JDK 1.2, there is an extension of the ClassLoader class that any Java developer can use as the superclass of her own class loader: the SecureClassLoader class (java.security.SecureClassLoader).
In terms of security, the benefit of the SecureClassLoader class comes because it is fully integrated with the notion of protection domains that was introduced in 1.2. We'll discuss this integration more fully in Chapter 5, "The Access Controller", when we have an understanding of what a protection domain is.
The SecureClassLoader class provides this new method:
Define a class that is associated with the given code source. If the code source is null, this method is the equivalent of the defineClass() method in the base ClassLoader class. We'll defer showing an example of this method to Chapter 5, "The Access Controller", when we discuss code source objects.
As our first example of a class loader, we'll use the same paradigm for loading classes that a Java-enabled browser uses, namely an HTTP connection to a web server:
public class JavaRunnerLoader extends SecureClassLoader { protected URL urlBase; public boolean printLoadMessages = true; public JavaRunnerLoader(String base, ClassLoader parent) { super(parent); try { if (!(base.endsWith("/"))) base = base + "/"; urlBase = new URL(base); } catch (Exception e) { throw new IllegalArgumentException(base); } } byte[] getClassBytes(InputStream is) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); BufferedInputStream bis = new BufferedInputStream(is); boolean eof = false; while (!eof) { try { int i = bis.read(); if (i == -1) eof = true; else baos.write(i); } catch (IOException e) { return null; } } return baos.toByteArray(); } protected Class findClass(String name) { String urlName = name.replace('.', '/'); byte buf[]; Class cl; SecurityManager sm = System.getSecurityManager(); if (sm != null) { int i = name.lastIndexOf('.'); if (i >= 0) sm.checkPackageDefinition(name.substring(0, i)); } try { URL url = new URL(urlBase, urlName + ".class"); if (printLoadMessages) System.out.println("Loading " + url); InputStream is = url.openConnection().getInputStream(); buf = getClassBytes(is); cl = defineClass(name, buf, 0, buf.length, null); return cl; } catch (Exception e) { System.out.println("Can't load " + name + ": " + e); return null; } } }
The key decision in using this class loader is where the classes are located--that is, the URL that needs to be passed to the constructor. If we were using this class loader in a browser, that URL would be the applet's CODEBASE; for an application, this location is up to the application to decide, using whatever means it deems appropriate (in the JavaRunner application, we used a command-line argument for that purpose). Note that the URL that is passed to the constructor must be a directory; in order to compose that directory into a URL later in the findClass() method, the name must end with a slash.
The logic of the findClass() method itself is simple: we need to convert the class name (e.g., com.XYZ.HRApplet) to a URL, which we can do by replacing the package-separating periods with slashes. Once the URL has been created, we simply obtain an input stream to the URL, read the bytes from that stream, and pass the bytes to the defineClass() method.
Note that the findClass() method encompasses most of the logic that is necessary for the lookupData() method we'd need if we were writing a 1.1-based class loader. The only difference for a 1.1-based class loader is that we would not need to call the defineClass() method, as that is called in our 1.1-based implementation of the loadClass() method.
The implementation we've just shown is the basis for the implementation of the URLClassLoader class. The basic difference between the two is that our implementation operates on a single URL, while the URLClassLoader class operates on an array of URLs. The URLClassLoader class can also read JAR files while our present implementation can only read individual class files; we'll remedy both those situations in the next section.
When we discussed the algorithm used to load classes, we mentioned that you could test to see if the class loader was allowed to access or define the package that the class belonged to. You might, for example, want to test whether the program should be allowed to access classes in the sun package, or define classes in the java package.
It is up to the author of the class loader to put these checks into the class loader--even in 1.2. In 1.2, if you want to make the check for package access, you can do that by calling the checkPackageAccess() method of the security manager in the same way that we called the checkPackageDefinition() method, but that will only prevent you from accessing classes that aren't found by the system class loader. Alternately in 1.2, you can use the newInstance() method of the URLClassLoader class, which makes such a check; or you can override the loadClass() method itself to provide such a check, as we showed earlier. In 1.1, of course, you have to write the loadClass() method from scratch, so you can call the security manager or not, as you deem appropriate.
In the case of defining a class in a package, the necessary code in a 1.2-based class loader must be inserted into the findClass() method as we did in our example class loader. Note that class loaders that are created by calling the constructor of the URLClassLoader class do not make such a call; they allow you to define a class in any package whatsoever.
For the Launcher (and any applications built on the URLClassLoader class), then, the default security model does not perform either of these checks. This is unfortunate: if a program is allowed to define a class in the java package, then that class will have access to all the package-protected classes and variables within that package, which carries with it some risk. The reason this model is the default has to do with the way in which the access controller defines permissions; we'll explore it more in depth when we write our own security manager in Chapter 6, "Implementing Security Policies".
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |