3.4. Implementing a Class LoaderPart 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. 3.4.1. Implementing the ClassLoader ClassAside 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. 3.4.1.1. Protected methods in the ClassLoader classIn order to implement a class loader, we start with 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.
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: Class Definitionprotected 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:
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. 3.4.2. Implementing the SecureClassLoader ClassStarting 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. 3.4.2.1. Protected methods of the SecureClassLoader classThe SecureClassLoader class provides this new method:
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: Class Definitionpublic 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. 3.4.3. Implementing Security Policies in the Class LoaderWhen 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. |
|