One of Java's main features is its ability to move code over a network and run that code. Unlike other languages, Java has been designed to do this securely. Java security has evolved over time; recent releases provide fine-grained security features that enable implementation of a flexible policy decoupled from the implementation mechanism.
Java security evolution and concepts: Read the whole series!
- Part 1: Learn computer security concepts and terms in this introductory overview
- Part 2: Discover the ins and outs of Java security
- Part 3: Tackle Java applet security with confidence
- Part 4: Learn how optional packages extend and enhance Java security
- Part 5: J2SE 1.4 offers numerous improvements to Java security
This article, Part 2 of a series, will cover the various aspects of securely running Java code downloaded from a network. Although mobile code is not a revolutionary concept, Java and the Internet present some unique challenges to computer security. The evolution of the Java architecture and its impact on security, different security APIs and tools, and applet security will be covered in this series.
This is not intended to be a comprehensive guide to computer security, a multifaceted issue encompassing several disciplines, departments, and cultures. Investments in security technologies should be followed by investments in personnel training, strict enforcement of policies, and periodic review of the overall policy.
Language vis-a-vis runtime security
Although enforcement of policies during code execution is a substantial part of security, proper security starts at the very beginning, during the generation of byte code. A language's type safety, which is enforced by the compiler and checked by the runtime environment, proves critical to an overall secure environment. Many of computing's earliest security breaches stemmed from the ability to easily overflow buffers or access memory unimpeded, situations caused in part by a language's poor type safety and inadequate enforcement in the executing environment.
Java security manifests itself in the following forms: protection built into the language, building blocks for a flexible secure environment, and protection against accidental or malicious attacks to the language and platform. Simply put, Java security should ensure that the specifications for the language and virtual machine (VM) are followed.
Despite the safety checks enforced by the compiler, the VM must still be able to deal with faulty byte code, whether generated accidentally or maliciously. Below, we'll examine the enforcement of runtime checks during execution and the evolution of the runtime environment to support the design of a very flexible security policy.
As for language security inherent in the language's design, Java's design was heavily influenced by C and C++ -- particularly their weaknesses. Consequently, the Java compiler generates warnings for uninitialized variables. The language itself is strongly typed, with many unsafe constructs omitted or modified; for example, array accesses are done with index checking. Moreover, because memory deallocation in Java is the garbage collector's responsibility rather than the programmer's, Java avoids many common programming errors in C and C++ caused by faulty memory deallocation. Finally, the compiler enforces exception-catching. This discipline of catching and fixing potential errors may not have direct security implications. However, an unhandled error might lead to unpredictable behavior, which, from a security standpoint, should be avoided.
Evolution of Java security
Some security features touted in recent Java releases were also available in earlier versions. However, using them involved a considerable amount of additional programming and a good understanding of the security model. Generally, Java has provided access to the language and the runtime environment conservatively, which is good from a security perspective, although somewhat restrictive of the ability to support different policies.
It's necessary for a security designer to understand Java security evolution, as many enterprises may have to support pre-Java-2 releases for years to come. So this article will concentrate on Java 2 Security, but only after an overview of security in previous releases.
JDK 1.0 security -- Java security hits the sandbox
JDK 1.0 featured the sandbox security model, as seen in Figure 1. The sandbox model confines Java applets, potentially dangerous or not, to a strictly defined arena where they cannot affect other system resources. (For more details of the sandbox model, including its limitations, see the "Sidebar 1: Sandbox model for security.")
Since applications load locally, they, unlike applets, need not be deemed untrustworthy and enjoyed unlimited access to all resources in JDK 1.0. A consistent security policy for applets and applications was therefore not supported by this model.
JDK 1.1 Security -- all or nothing
JDK 1.0's sandbox model provided virtually no flexibility. In an attempt to overcome this, JDK 1.1 introduced the signed applet, illustrated in Figure 2. A signed applet is an applet packaged as a Java Archive (JAR) file and signed with a private key. The signed applet enjoys unlimited access, just like a local application, provided the corresponding public key is trusted in the executing environment. Unsigned applets default back to the sandbox model.
JDK 1.1's security model was less restrictive than the sandbox model and provided for slightly more consistent treatment of applets and applications. However, it had one major disadvantage: applets either received unlimited access or were confined to the sandbox -- there was no option for selective access to resources. This model was another example of an inflexible implementation where the policy was forced by the mechanism.
Java 2 Security -- fine-grained security
The Java 2 Security model, as illustrated in Figure 3, provides for a consistent and flexible policy for applets and applications. While applications still run unrestricted by default, they can be subjected to the same policy as applets.
The Java 2 model also introduces the concept of a
ProtectionDomain, which permits a highly flexible security policy decoupled from its implementation.
Overview of Java 2 security
The various features of the Java 2 Runtime Environment's security model can be seen in Figure 4. As a general caveat, some of the issues discussed might not be part of the specification per se, but still peculiar to most implementations.
Since Java code can be imported from anywhere in the network, it is critical to screen the code to be sure that it was produced by a trustworthy compiler. The byte-code verifier, sometimes referred to as a mini-theorem prover, tries to prove that a given series of Java byte codes are legal.
Among other things, it verifies the format of the class file - it must have the right length, the correct magic numbers, no operand stack overflows and underflows, and so on. In short, the verifier confirms or denies that the class file is consistent with the specifications. Although Figure 4 appears to indicate that byte-code verification occurs when the class is loaded, some of these checks may be delayed until just before the byte code is first executed.
Even though the byte-code verifier serves an important purpose, it is not very interesting from a programming standpoint because its behavior cannot be altered programmatically. The behavior may be altered with command line options on the interpreter, when applicable.
The ClassLoader, which loads Java byte codes into the JVM, is an important link in the security chain. It works in conjunction with the SecurityManager and the access controller to enforce security rules. Notice in Figure 4 that the ClassLoader is involved in enforcing some security decisions earlier in an object's lifetime than the security manager. Also, information about the URL from which the code originated and the code's signers is initially available to the ClassLoader.
It is possible to implement a customized ClassLoader or a subclass from
java.security.SecureClassLoader to provide security features beyond those offered by the standard Java 2 Security model.
The ClassLoader, as its name indicates, loads classes into the VM. It is also responsible for the concept of namespaces at runtime, which are created by packages. With namespaces, identically named identifiers can refer to different objects.
Since a ClassLoader is itself a class, some bootstrapping is necessary early on. The ClassLoader, referred to as the primordial class loader and usually written in a native language, loads the bootstrap classes in a platform-dependent manner.
Some classes defined in the
java.* package are essential to the JVM and the runtime system. These are referred to as system classes, loaded by the System ClassLoader. The terminology sometimes extends to all classes found in the CLASSPATH environment variable. Typically, a ClassLoader hierarchy is created as more classes are loaded.
Since Java code can be downloaded over a network, the code's origin and author are critical to maintaining a secure environment. Consequently, the object
java.security.CodeSource fully describes a piece of code. The CodeSource encapsulates the code's origin, which is specified as an URL, and the set of digital certificates containing public keys corresponding to the set of private keys used to sign the code.
Many access-control decisions are based in part on this property.
Permission classes are at the very core of Java security and represent access to various system resources such as files, sockets, and so on. A collection of permissions can be construed as a customizable security policy for an installation.
For example, permission may be given to read and write files in the
/tmp directory. Permission classes are additive in that they represent approvals, but not denials. It's possible to explicitly permit the reading of a particular file, but not to explicitly deny the reading of that file.
A number of permission classes are subclasses of the abstract
java.security.Permission class, examples of which include
AWTPermission, and even customized protections like
SendMailPermission. For an exhaustive list of permissions, see Resources for a link to Li Gong's Inside Java 2 Platform Security.
It's possible to associate permissions with classes; however, it's more flexible to group classes into protection domains and associate permissions with those domains. A class's mapping to a domain occurs before the class is usable and is immutable thereafter. For instance, system classes can be effectively grouped under the system domain.
This relationship between the class and the permissions via the protection domain provides for flexible implementation mechanisms.
The numerous mappings of permissions to classes are collectively referred to as policy. A policy file is used to configure the policy for a particular implementation. It can be composed by a simple text editor or using
policytool, a tool bundled with the Software Development Kit (SDK), which we'll discuss in a future article.
As seen in Figure 4, the class
java.lang.SecurityManager is at the focal point of authorization -- the implementation of the sandbox model in JDK 1.0 is a good example.
Prior to Java 2,
SecurityManager was abstract; vendors had to create concrete implementations. In Java 2,
SecurityManager is concrete, with a public constructor and appropriate checks in place to ensure that it can be invoked in an authorized manner.
SecurityManager consists of a number of check methods. For example,
checkRead (String file) can determine read access to a file. The
checkPermission (Permission perm, Object context) method can check to see if the requested access has the given permission based on the policy. This method is relegated to the access controller in the default implementation. The access controller will raise an exception if the requested permission cannot be granted. Refer to the "Sidebar 2: Security exceptions" for an overview of security exceptions.
java.security.AccessController class is used for three purposes:
- To decide whether access to a critical system resource should be allowed or denied, based on the security policy currently in effect
- To mark code as privileged, thus affecting subsequent access determinations
- To obtain a snapshot of the current calling context, so access-control decisions from a different context can be made with respect to the saved context
SecurityManager can be overridden, the static methods in
AccessController are always available. If you wish a particular security check to always be invoked, regardless of which security manager is in vogue, the
AccessController methods should be called.