If you are building an application that loads and runs Java programs from the network (or from any source that is not trustworthy), it's important that you think about the security policy you need and build your application to enforce it. It's a difficult job; security policy decisions are subtle and complicated. You also need to take special care in the implementation, because bugs in this part of your program can have serious consequences. Fortunately, a lot of the tricky parts of a security policy implementation are in the standard Java libraries. This chapter explains the Java security model and shows how to write the code that makes security decisions for your application. It also presents some tips on formulating security policies.
Before getting into the details of how specific security checks and policies are implemented in Java applications, it's important to have a clear idea of the basic structure of the language security model, which provides the fundamental assumptions upon which a policy implementation rests.
The first line of defense against untrusted programs in a Java application is a part of the basic design of the language: Java is a safe language. When programming language theorists use the word "safety," they aren't talking about protection against malicious programs. Rather, they mean protection against incorrect programs. Java achieves this in several ways. The language does not allow programmers to manipulate pointers directly (although they are used extensively behind the scenes). Array references are checked at runtime to ensure that they are within the bounds of the array. Casts are carefully controlled so that they can't be used to violate the language's rules, and implicit type conversions are kept to a minimum. Memory management is automatic. All these qualities make Java a "safe" language. Put another way, they ensure that code written in Java actually does what it appears to do, or fails. The surprising things that can happen in C, such as continuing to read data past the end of an array as though it were valid, cannot happen. In a safe language, the behavior of a particular program with a particular input should be entirely predictable-with no surprises.
Inserting rules about proper language behavior into the language specification is a good thing, but it's also important to make sure that those rules aren't broken. Checking everything in the compiler isn't good enough, because it's possible for someone to write a completely new compiler that omits those checks. For that reason, the Java library carefully checks and verifies the bytecodes of every class that is loaded into the virtual machine, to make sure that those bytecodes obey the rules. Some of the rules, such as bounds checking on references to array elements, are actually implemented in the virtual machine, so no real checks are necessary. Other rules, however, must be checked carefully. One particularly important rule that is verified rigorously is that objects must be true to their type-an object that is created as a particular type must never be able to masquerade as an object of some incompatible type. Otherwise, there would be a serious loophole through which explicit security checks could be bypassed.
The final part of the Java security model is the implementation of the Java class library. Classes in the library provide Java applications with their only means of access to sensitive system resources, such as files and network connections. Those classes are written so that they always perform security checks before granting access.
Application authors can write their own native methods, which extend the Java library and provide access to new resources. It's important to remember security issues when doing so, and Chapter 33, "Securing Your Native Method Libraries," explains how to make your library extensions as secure as the core Java library.
It's impossible for the Java application programmer to alter the basics of the Java security model without delving into native methods, but you can modify the security policy: the way access decisions are made.
Ultimately, decisions about access to sensitive resources are made by the security manager, which is an instance of the SecurityManager class. Not just any instance will do: the security manager for the application is set by using System.setSecurityManager and accessed by using System.getSecurityManager. Here's how to install a security manager in your application:
System.setSecurityManager(new mySecurityManager());
Any Java method can query the security manager, but it's crucial that the methods that provide access to sensitive system resources query the security manager before they permit the access. As an example of how it works, here's the File.delete method:
/**
* Deletes the specified file. Returns true
* if the file could be deleted.
*/
public boolean delete() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkDelete(path);
}
return delete0();
}
delete0 is the method that really does the work of deleting the file. Before calling it, the delete method checks with the security manager to see whether the operation will be permitted. If everything is fine, the security manager's checkDelete method simply returns, and the delete0 method is called. If the operation is not allowed, checkDelete throws a SecurityException. Because delete makes no attempt to catch the exception, it propagates up to the caller, and delete0 is never called.
When an applet is running in an application such as Netscape Navigator or appletviewer, it is free to call methods such as File.delete. However, when the delete method checks with the security manager, it will see that the request is being made by an applet and throw a SecurityException. The applet is free to catch the exception and ignore it, but the exception prevents the delete0 method from being called, so the applet can't actually delete the file.
In the delete method, if there is no security manager defined, access is always granted. The same is true in all the methods that perform security checks. If no security manager is defined, everything is allowed-even creating a security manager. Thus, it's important that any application that is going to be loading untrusted classes create a security manager before the first untrusted class is loaded into the virtual machine.
What's to keep an untrusted class from replacing the security manager? Once a security manager has been set by using System.setSecurityManager, it is always a security violation to try to set a new security manager. There can only be one security manager in an application. Thus, if it's desirable to adjust the security policy of an application while it is running, those adjustments must be catered for in the security manager itself; they can't be accomplished by changing the security manager.
Typically, application-specific classes do not need to call the security manager. Those classes call the Java library classes to access sensitive resources, and the library classes call the security manager. Classes that use factory objects are an exception. (See Chapter 17, "Network-Extensible Applications with Factory Objects," for more information.) Classes that use factory objects usually provide a method for setting the factory object, such as the URLConnection.setContentHandlerFactory method. Such methods should make a call to SecurityManager.checkSetFactory(). They should also refuse to replace an existing factory; just as with the system security manager, it should be an error to set a factory object when one has already been set. Here's the relevant method from URLConnection:
public static synchronized void
setContentHandlerFactory(ContentHandlerFactory fac) {
if (factory != null) {
throw new Error("factory already defined");
}
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkSetFactory();
}
factory = fac;
}
The SecurityManager class, which is a part of the core Java library, is an abstract class, so it cannot be used directly. Nor would you want to-its implementation disallows everything, no matter what the source, so if you install it, you won't have a very useful application. To implement a security policy, you must subclass SecurityManager, overriding the methods to determine resource access according to policies that you define.
When the security manager has to decide whether to allow or deny access to a resource, what information can it use to make the decision?
The security manager can investigate the current execution environment
to learn what classes currently have methods executing on the
execution stack. It can also learn about the ClassLoader
objects that loaded those classes into the runtime environment.
If your application has class loaders that load from the network
or other untrusted sources, you can arrange for those class loaders
to keep track of the source of each different class for use by
the security manager. The section Implementing Security Managers,
later in this chapter, discusses this in more detail.
Note |
In the default Java environment, which is presented to stand-alone applications (as opposed to applets, for example), there are no class loaders. Classes that are found in the CLASSPATH are loaded by the virtual machine implementation. This is sometimes referred to as the "primordial" class loader, but from the security manager's viewpoint, no ClassLoader object is involved at all. Without access to a ClassLoader object, the security manager cannot learn any details about the origin of the class. For this reason classes that are found in the CLASSPATH are always trusted. |
The JDK appletviewer is typical of applications that load code from untrusted sources. It creates a separate class loader for each base URL from which it needs classes, and each class loader is responsible for only those classes that are fetched from that URL. The section Implementing Class Loaders, later in this chapter, discusses this in more detail.
You don't have to base your security model on the network source of classes. On a multiuser machine, it's possible to place restrictions on classes based on the ownership of the class file. (This only works for classes that are not found in the CLASSPATH; see previous Note.) It will someday also be possible to determine the real source of classes, using digital signature techniques, so that security can be relaxed for classes certified by someone you trust (see Chapter 22, "Authentication, Encryption, and Trusted Applets," for more information).
The SecurityManager class provides checks for several types of resources:
When you're trying to settle on the details of a security policy, it helps to know exactly which resources are protected and when the security checks are made. Table 21.1 lists all the access check methods provided by the security manager, a description of functions performed by each, and the Java core library methods that call them.
In order to design a good security policy, you should understand the different reasons for securing all the resources listed previously. Without that understanding, it's easy to design a policy with unforeseen weaknesses. The following paragraphs describe some of the issues involved and the possible attacks that could be made if the Java security features were relaxed.
It's fairly obvious that file system access must be restricted to prevent both theft and destruction of valuable information. However, the Java library provides the means to secure some seemingly innocuous types of accesses. Classes can be prohibited from learning whether a file exists, the length of a file, the last-modified date, or whether a file is a directory. For many files, which simply contain user data, it might sometimes be enough to protect the contents and not any of that other information. For some special system files, however, even knowledge of the file's existence on a particular system might be valuable information. The existence of a particular file might be an indicator that a software package has been installed on the system. For certain files, which consist of fixed-length records, the length of the file reveals how many records are in the file. These examples illustrate the difficulty of deciding what information should be protected and what should not. It's important that you protect file access that is not crucial to your application. Similarly, you should allow access only to system commands that are essential to your application and those that you are confident do not pose a risk. Among other serious problems, system commands can be used to bypass file system security.
Network access involves more subtle issues. For example, many Java developers have been confused by Netscape Navigator's policy of permitting classes to open network connections only to the same host from which they were loaded. If you work in an organization that uses a firewall for security between the internal network and the Internet, you probably understand. Behind firewalls, it's common for access between machines to be loosely controlled. Once an applet begins running on one machine behind the firewall, access to other machines in the organization might be possible via the network.
It's possible to find other policies that offer more flexibility while still maintaining reasonable security. You might permit a list of prohibited network addresses (or address ranges) to be specified via a configuration option. If your application is strictly for use within your organization, you might permit applets always to access sites outside your firewall, but permit internal access only if the applet originated within the firewall. You still have to think seriously about the security issues. For instance, the application with the configuration option could easily be compromised by a user who didn't understand the issues (see Chapter 20, "A User's View of Security," for a discussion of user perception of security issues).
Another potential problem with allowing network access is the possibility that a malicious applet could masquerade as the application's user. Because the communication would actually be coming from the user's machine, it would be difficult to determine that it was not actually the user who initiated it. For example, while browsing the Web, you could execute an applet that would send mail on your behalf. For this reason, it's a good idea to prohibit connections to certain reserved port numbers (such as TCP port 25, the mail transport protocol port) even when other network access is permitted. That measure doesn't completely solve the problem, but it helps. The real solution to the problem will come when it becomes common for people to use digital signatures on all their communications; unsigned mail will be immediately suspect, rather than being accepted as it is today.
Thread and thread group access is somewhat less complicated. There is little reason for downloaded code to be given access to the applications's threads. Downloaded code should probably be run in a thread group of its own and should only be allowed to modify that group. You may choose to allow one applet to modify the threads or thread groups of other applets, depending on the degree to which applets need to interact. However, there is little risk of serious data theft or loss involved in thread access. It is important, however, to restrict the maximum priority of threads belonging to untrusted code, to ensure that they don't become nuisances (see the section Denial of Service Attacks, later in this chapter).
Factory objects dynamically create specialized objects of a general type, choosing the specific type based on information provided at runtime. (The new operator makes up its mind at compile time.) If an untrusted object were to be installed as a factory, it might create objects that spoof important functions, pretending to perform the function properly while actually doing something else.
Method | Description | Parameters | Calling Methods |
Local File System Access | |||
checkRead(int) | Checks read access to the specified file descriptor | A system-dependent file descriptor | FileInputStream(FileDescriptor) |
checkRead(String) | Checks read access to the named file | A system-dependent file name | File.exists(),
File.canRead(), File.isFile(), File.isDirectory(), File.lastModified(), File.length(), File.list(), FileInputStream(String), RandomAccessFile(String, String) |
checkRead(String, Object) | Checks read access to the named file in both the current context and the context represented by the Object parameter1 | A system-dependent file name and an object representing a security context | (Not currently called) |
checkWrite(int) | Checks write access to the specified file descriptor | A system-dependent file descriptor | FileOutputStream (FileDescriptor) |
checkWrite(String) | Checks write access to the named file | A system-dependent file name | File.canWrite(), File.mkdir(), File.renameTo(File), FileOutputStream(String), RandomAccessFile(String, String) |
System Access | |||
checkExec(String) | Checks whether to allow execution of a system command | A system-dependent command line | Process.exec(String, String[]), Process.exec(String[], String[]) |
checkPropertiesAccess() | Checks access to the list of system properties | None | System.getProperties() System.setProperties() |
Network Access | |||
checkAccept(String, int) | Checks whether a connection from a particular host and port may be accepted | A host name and port number | ServerSocket.accept() |
checkConnect(String, int) | Checks whether an attempt to connect to a particular host and port will be allowed | A host name and port number | InetAddress.getByName(String) InetAddress.getAllByName (String) Socket(String, int, boolean) Socket(InetAddress, int, boolean) DatagramSocket.send (DatagramPacket) DatagramSocket.receive (DatagramPacket) |
checkConnect(String, int, Object) | Checks whether an attempt to connect to a particular host and port will be allowed in both the current execution context and the context represented by the Object parameter1. | A host name, port number, and object representing a security context | (Not currently called) context and the context |
checkListen(int) | Checks whether listening on a particular port is allowed | A port number | ServerSocket(int, int) DatagramSocket(int) |
Thread Manipulation | |||
checkAccess(Thread) | Checks access to thread operations | A thread | Thread.checkAccess()2 Thread.stop()Thread.stop(Throwable) Thread.suspend()Thread.resume() Thread.setPriority(int) Thread.setName(String) Thread.setDaemon(boolean) |
checkAccess(ThreadGroup) | Checks access to thread group operations | A thread group | ThreadGroup.checkAccess()2 ThreadGroup(ThreadGroup, String) ThreadGroup.setDaemon(boolean) ThreadGroup.setMaxPriority(int) ThreadGroup.stop()ThreadGroup.suspend() ThreadGroup.resume() ThreadGroup.destroy() Thread.init(ThreadGroup, Runnable,String) |
Factory Object Creation | |||
checkSetFactory() | Checks access to replace network-related factory objects | None | ServerSocket.setSocket Factory(SocketImplFactory) Socket.setSocketImpl Factory(SocketImplFactory) URL.setURLStreamHandler Factory(URLStreamHandler Factory) URLConnection.setContent HandlerFactory(Content HandlerFactory) |
Interpreter Manipulation | |||
checkLink(String) | Checks whether loading a dynamic library will be allowed | A filename or library name | Runtime.load(String) Runtime.loadLibrary(String) |
checkExit(int) | Checks access to shutting down the Java interpreter | An exit status | Runtime.exit(int) |
checkCreateClassLoader() | Checks whether a new class loader may be created | None | ClassLoader() |
checkPackageAccess(String) | Checks whether classes in a package may be loaded | A package name | (Not currently called) |
checkPackageDefinition (String) | Checks whether a new class may be defined in a package | A package name | (Not currently called) |
Window Creation | |||
checkTopLevelWindow() | Checks whether a new toplevel window may be created3 | None | Window()
Window(Frame) |
1The nature of security context objects is application-dependent, and only the security manager needs to understand them. The security context objects are acquired by calling the SecurityManager.getSecurityContext() method. In a typical application that bases its trust of classes on the network host from which they were loaded, URL or InetAddress objects might be valid security context objects. | |||
2Threads and thread groups call their respective SecurityManager.checkAccess methods to check on many different operations. Each encapsulates that call within its own checkAccess methods, which make the only direct calls to the security manager. All the other methods listed make indirect calls through the methods in the Thread and ThreadGroup classes. | |||
3Unlike the other security check methods, checkTopLevelWindow returns a value. If creating a top-level window is not permitted at all, the method throws a SecurityException, but if the window creation is permitted, the method returns a boolean value. If the value is false, the window is adorned with a prominent warning that the code in control of the window is not trusted. |
For example, the SocketImplFactory object creates SocketImpl objects when new sockets are created. The SocketImpl object, as its name suggests, provides the actual implementation of the socket. (This seems like a funny way to do things, but it permits subclassing SocketImpl and adding knowledge of how to traverse a corporate firewall. A new SocketImplFactory would return the appropriate kind of socket implementation, depending on whether the address was outside the firewall.) If an untrusted object were substituted for the trusted SocketImpl, it could redirect the socket to another host or alter the data being sent across the socket, without the user of the application being aware of the deception.
Because ClassLoader objects cooperate with the security manager to enforce security policy (see the section Implementing Class Loaders, later in this chapter), an untrusted ClassLoader could compromise the application's entire security architecture. Additionally, because packages are an important part of the Java protection mechanism, ClassLoader objects should check with the security manager before creating a new class within a package and before allowing an untrusted class to use a particular package (using the checkPackageDefinition and checkPackageAccess methods of the security manager, respectively).
Native methods are written in some language other than Java. They might be a source of security problems-they can bypass the Java security mechanisms, although they don't have to (see Chapter 33 for more information). Because of this, loading a native method library might compromise the security of your application.
It's fairly obvious why untrusted code shouldn't be allowed to cause the Java virtual machine to exit. Not only would it be extremely annoying, but a user could lose a significant amount of work, or an important server could be taken out of service.
Java gives the security manager a say in the creation of new top-level windows, with an extra twist. Not only can the security manager permit the request or refuse it outright, it can compromise by allowing the window creation so long as a warning is displayed in the window. The text of the warning is taken from a system property. Refusing window creation might be useful in controlling some kinds of nuisance attacks, such as an applet that filled your screen with thousands of useless, unresponsive windows, obscuring the user's work and making it difficult even to shut down the application. The warning helps to make the user aware of possible deceptions. Both of these kinds of problems are discussed in the next section.
Theft and destruction of information aren't the only unpleasant things that a malicious applet can do. An applet can deceive a user into voluntarily providing sensitive information. Without proper controls, untrusted code could mount what is called a denial-of-service attack by attempting to consume such a large amount of some resource (CPU cycles, memory, network bandwidth, or even screen space) that no more is left for the user of your application.
People are often easily deceived. That's why the AWT provides a way for the security manager to attach a warning to windows owned by untrusted code. Take advantage of that capability. It's not a perfect answer, though. For example, applets that run inside the main Netscape window, rather than creating their own top-level windows, display no such warning, but can do most of the same things. As more and more powerful applications become available in applet form, users may become accustomed to having mostly "untrusted applet windows" on their screens, and the warnings will lose their force. Ultimately, the solution to deception attacks will come with increased user awareness of the problem.
Denial-of-service attacks are a little easier to control. There are several steps that you can take to prevent such attacks, or at least severely limit them. The security manager is consulted whenever the following types of resources are allocated: threads, thread groups, network connections, and windows. If the application keeps track of applets or other downloaded programs appropriately, the security manager could track an applet's usage of certain resources and eventually begin denying the requests. This technique could prevent many simple abuses.
Unfortunately, the security manager doesn't have all the information that it needs to do a good job of resource tracking. When the checkAccess(ThreadGroup) method is called, for example, the security manager doesn't know whether a thread is being created or an entire thread group is being destroyed. Sockets don't keep track of the number of bytes that have passed through them, and threads don't keep track of their CPU utilization.
It might be possible to add some of this resource tracking yourself by being exceptionally tricky. It would be much better, however, if a future Java release provided better ways to track the resources used by particular threads or thread groups or by classes from a particular source. That would give application authors much more power to deal effectively with denial-of-service attacks.
Currently, the best available solution is to make sure that the user of the application has good control over what is happening. Downloaded code should run in thread groups with lowered maximum priorities to ensure that trusted application code will always get a chance to run. Along with that, it would be nice for users to be able to pop up some sort of status display, showing the status of all thread groups and the source of the classes running in each. As a last resort, knowledgeable users could use such displays to shut down nuisance applets. That's definitely not the best solution, but it's wise to provide some way for the user to retain ultimate control.
You've probably realized by now that security policies can be very complex, with many subtleties. If you are content to support applets that can put on a good show, but can't do anything really useful, it's not too bad. On the other hand, if you want downloaded applications to be able to do useful things without doing harmful things, you have some careful thinking to do. Is there any way to make it simpler?
Fortunately, the answer is "yes." Network security is never easy, but there are ways of organizing your thoughts about security policy to keep the complexity from overwhelming you.
Some types of Java applications will always have complex security issues to deal with, because they have a broad purpose. An example is a desktop manager for a cheap Internet terminal device. Because such devices don't have much local storage, they have to use the network as a source for all sorts of full-fledged applications. In that environment, it's impossible to predict in advance what a downloaded application will need to do.
If you're building a more specialized application, you can make some simplifying assumptions about the requirements of downloaded programs. In particular, you should design your security policy around the kinds of objects that the users of your applications will be thinking of-application-level resources and abstractions. This will not only help you think about the security policy, it will also help users understand the implications of security configuration decisions that they might have to make.
Once you identify some application-level abstractions upon which to base your security model, write special classes that provide access to system resources based on those abstractions. These classes should implement their own security checks, based on their understanding of the resources. Take care that those security restrictions can't be bypassed by subclassing. Then write your security manager to trust those classes.
Here's an example. Think about a calendar management program that permits "agents" from other people to enter your system, querying and possibly modifying your calendar. Such a system would work much like current distributed calendar management systems, which work using a client-server model. However, by requiring other users' agents to come to your machine to query your calendar, the system could be more convenient while doing a better job of protecting your privacy.
Suppose a friend wants to invite you to the office Christmas party. Her agent might enter your system and do something like this:
Calendar joecal = new Calendar("joe");
// 20 December 1996, 7:00 p.m. Month 0 is January, month 11 is December!
Date begin = new Date(96, 11, 20, 19, 0);
Date end = new Date(96, 11, 20, 24, 0); // midnight
if (!joecal.busy(begin, end)) {
// no plans, so you're invited!
joecal.invite(begin, end, "Winter Garden Ballroom",
"Office Christmas party", true);
// The final parameter means "rsvp".
}
In order to handle this, the Calendar object called joecal must follow these steps:
In designing the security model for this application, you shouldn't really think in terms of files and directories; that will make things much more complex, and you'll also miss some important opportunities. You should think in terms of calendars, appointments, and invitations.
The calendar object should have its own security model, making sure that it reads only calendar files and no others. It should provide a limited interface to the calendars-querying whether a particular time period is free, searching for free time periods of a given duration during a particular week, and recording an invitation for a particular event.
The security manager should continue to disallow file system access as usual, except in the case where the request is actually being made from the Calendar class. (The security manager has access to enough information about the execution context to be able to verify that case; this is discussed later in this chapter.) Because the security manager and the Calendar class are both part of the same application, the Calendar class can be trusted.
The interesting part of the whole thing is the Calendar class's security model. Besides limiting access to real calendar files, it could perform other functions as well. It wouldn't have to obey every request blindly; just like the Java security manager, it could choose to accept or deny a request based on the source of the requestor. It could even lie on your behalf.
If you had a good relationship with your boss (and if you're the kind of person who carefully records every commitment in your calendar), it might be a lot easier for both of you if your boss could simply enter meetings into your calendar when necessary. You would be notified, but you wouldn't have to be involved when the meeting was scheduled. You probably wouldn't want your boss scheduling meetings for you on weekends or holidays, however. Additionally, if you turned out to already have a commitment for a particular time, you might not want it known that you had scheduled an interview for a new job. The Calendar class should let you specify that your boss has, for example, probe, freesearch, and add access for work hours on your calendar, but not for nights, weekends, or holidays. Read access, by which your boss might learn details about a commitment, could be withheld.
Other alternatives are possible. If there's a manager in another department who enjoys long, unfocused, boring meetings, and likes to invite you along for some reason, you could arrange for your calendar to handle invitations from that manager specially. It could forward the invitations to your electronic mail, rather than recording them directly, so that you could invent an excuse. It could even pretend to find a commitment no matter what time was queried.
If you were designing a security model for this application by thinking in terms of the low-level system resources that the SecurityManager class understands, things would be very complex. For one thing, the application requires finer granularity than the SecurityManager can provide-you want to restrict access not to an entire file, but to parts of the file. Thinking about security at the level of the application is a lot simpler, and it also permits you to offer additional privacy features and flexibility above and beyond what the built-in Java security model supports.
Unlike most other portions of an application, class loaders must work both sides of the security fence. They must take care to consult the security manager before allowing certain operations, and they must cooperate with the security manager to help it learn about classes and make decisions about access requests. They must also avoid breaking any of the assumptions about classes on which the security manager relies.
When defining a class, the class loader must identify the package in which the class belongs and call SecurityManager.checkPackageDefinition before actually loading the class into that package. Membership in a package gives a class special access to other classes in the package and can provide a way to circumvent security restrictions.
When the class loader defines a class, it must also resolve the class. Resolving a class involves locating and loading, if necessary, other classes that the new class requires. This is done by calling ClassLoader.resolveClass(Class). During the resolution process, the Java runtime calls the loadClass(String, boolean) method in the same ClassLoader that loaded the class currently being resolved. (If the boolean parameter is true, it means that the newly loaded class must be resolved also.)
The class loader must be careful not to load a class from an untrusted source that will mirror a trusted class. The CLASSPATH should be searched first for system classes. This is especially important during the resolution process.
Additionally, the class loader should check with the security manager about whether the class being resolved is even allowed to use the classes in the requested package. The security manager might wish to prevent untrusted code from using entire packages.
Here's an example of steps you might take to load a class securely:
protected Class loadClass(String cname, boolean resolve) {
// Check to see if I've already loaded this one from my source.
Class class = (Class) myclasses.get(cname);
if (class == null) {
// If not, then I have to do security checks.
// Is the requestor allowed to use classes in this package?
SecurityManager security = System.getSecurityManager();
if (security != null) {
int pos = cname.lastIndexOf('.');
if (pos >= 0) {
security.checkPackageAccess(cname.substring(0, pos));
}
}
try {
// If there's a system class by this name, use it.
return findSystemClass(cname);
}
catch (Throwable e) {
// otherwise, go find it and load it.
class = fetchClass(cname);
}
}
if (class == null) throw new ClassNotFoundException();
if (resolve) resolveClass(class);
return class;
}
In the example, the real work of actually retrieving a class and defining it is done in the fetchClass method. The primary security responsibility of that method is to call SecurityManager.checkPackageDefinition(package) before actually defining the class, as described previously.
The way this resolution process works (with the ClassLoader that loaded the class being responsible for resolving class dependencies) is one reason why applications typically define one class loader for each different source of classes. When a class from one source has a dependency on some class named, for example, MyApplet, it would probably be a mistake to resolve the dependency using a class with the same name from another source.
The other side of the class loader's responsibility for security is to maintain information about classes and provide that information to the security manager. The type of information that is important to the security manager depends on the application. Currently, most Java applications base security decisions on the network host from which a class was loaded, but other information may soon be used instead.
Implementing a security manager can involve a lot of work, but once you have designed a coherent security policy, it isn't particularly complicated. Most of the work involved stems from the fact that SecurityManager has a lot of methods that you need to implement.
Once the security manager has decided to allow an operation, all it needs to do is return. Alternatively, if the security manager decides to prohibit an operation, it just needs to throw a security exception. As mentioned earlier in this chapter, the decision is the hard part.
The security manager can examine the execution stack to find out which classes have initiated an operation. If an object's method is being executed at the time that the security manager is called, the class of that object is requesting the current operation (either directly or indirectly). The important thing about the objects on the stack, from the security manager's point of view, is not the objects themselves but their classes and those classes' origins. In Java, each object contains a pointer to its Class object, and each class can return its class loader via the getClassLoader() method. The implementation of SecurityManager uses those facts, along with native methods that can find the objects on the stack itself, to find out the classes and class loaders that have objects on the execution stack. Figure 21.1 depicts the Java execution stack while the security manager is executing.
Figure 21.1 : The security manager and the Java execution stack.
Note |
Because the security manager doesn't really care about the objects themselves-just the classes and class loaders-the documentation for the SecurityManager class blurs the distinction a bit. It refers to "the classes on the execution stack" and "the class loaders on the execution stack." The following paragraphs use the same phrases. Strictly speaking, the classes in question aren't actually on the stack, but they have instances that are. Likewise, the class loaders in question aren't really on the stack, but they are responsible for classes that are. It's just a lot easier to talk about "a ClassLoader on the stack" than "an object on the stack that is an instance of a class that was loaded by a ClassLoader." |
The JDK appletviewer application and Netscape Navigator 2.0 have simple security models: if a class is not a system class (that is, if it wasn't loaded from CLASSPATH), it isn't trusted and isn't allowed to do very much. If your security model is that simple, your security manager will be simple, too. Calling SecurityManager.inClassLoader() tells you whether the operation is being requested by untrusted code. It returns true if there is any class loader at all on the stack. Recall that system classes don't have a class loader, so if there's a class loader on the stack anywhere, there's an untrusted class in control.
If an operation is to be prohibited in general, but allowed if
it comes from a particular trusted class (as in the Calendar
example), you can investigate further. SecurityManager.class
LoaderDepth() tells you how deep on the stack the
first class loader is. Coupled with SecurityManager.classDepth(String),
it's possible to determine whether a particular trusted class
is really in control:
if (classDepth("COM.Neato.Calendar") < classLoaderDepth()) {
// The Calendar class is in control, so we can allow the request.
return;
}
else {
throw new SecurityException("attempted to read file" + filename);
}
The inClass(String) method might also be helpful in this situation, if you're confident that the class you're interested in doesn't call any untrusted classes along the way. Be careful, however, because inClass simply tells you that the specified class is on the stack somewhere. It says nothing about how deep the class is or what classes lie above it on the stack.
Currently, Java applications typically don't support multiple levels of trust-a class is either trusted or it's not. If you are designing an application that can verify the source of a class, you may suddenly need more information about the class loader responsible for the object requesting an operation. The currentClassLoader() method returns the ClassLoader object highest on the stack. You can query that object for application-specific information about the source of the class.
Finally, if all those other methods aren't enough to implement your security policy, SecurityManager provides the getClassContext() method. It returns an array of Class objects, in the order that they appear on the stack, from top to bottom. You can use any Class methods on them to learn various things: getName(), getSuperclass(), and getClassLoader(), among others.
Building your application's security manager takes work, and it can be complicated, but it doesn't have to be a nightmare. Just be sure to design a coherent, application-specific security policy first.
Any application that loads and executes Java code from untrusted sources must have an effective, coherent security policy. Designing and implementing the security policy are two of the most important tasks in Java application development.
An application security policy must build upon and interact with the Java security model. The SecurityManager class provides the basis for a security policy implementation, including the methods that actually make access decisions and utility methods that provide important information about the execution environment. ClassLoader objects, responsible for dynamically loading classes from various sources, cooperate with the SecurityManager by providing information about the origin of particular classes. By building specialized versions of SecurityManager and ClassLoader, you can implement the security policies that meet the needs of your application and your users.