If you've paid much attention at all to coverage of Java in the
technical and business press, you might have encountered discussions
on Java security, and read about malicious applets and so on. This is
an interesting and complex subject to contemplate, and we'll spend
several issues looking at it. There are multiple types and levels of
security to consider.
Let's first look at a short C program:
void main() { char c; int fd; char* p = 0; char buf[1024]; fd = open("data", 0); while (read(fd, &c, 1) == 1) *p++ = c; close(fd); } |
This is a mixture of C code and UNIX system calls. It opens a data
file, reads from it, and places the read characters into a character
buffer.
Unfortunately, this program has a bug, in that the statement:
p = buf; |
was omitted. So the program will write via the pointer p, but p is
null (0), and the bytes will get written into memory location 0 and
succeeding locations. If you're running on a DOS machine, this will
probably silently crash your computer. If you are on a UNIX machine
with virtual address space starting at 0, with text (the program
itself) in low memory, then perhaps you'll get a segmentation
violation error.
The point is that a language like C is "close" to the hardware. This
is literally true if running with DOS, somewhat less true if using
virtual memory and process protection. Direct use of system calls for
doing I/O in the example above is another illustration of this point.
This feature of C is both a strength and a weakness. For example, C
is a good language for writing I/O device drivers, because it's close
to the hardware.
Java takes a different tack. It has no user-visible pointers, and no
notion of an explicit memory address that you can manipulate or print
out. With Java it's still possible to make the dumb mistake found in
the example above, but the result would simply be an immediate null
pointer exception. There is no way to write a byte to physical or
virtual address 0.
Similarly, there is no direct access to system calls. A Java program
can certainly do I/O to disk files (applets are restricted in this
area), but it can't make system calls itself.
This insulation from the physical machine, via the Java Virtual
Machine, means that program execution tends to be safer as far as
program crashes and interference with other programs go. The tradeoff
is inability to control the actual physical machine, and some loss of
efficiency. This implies that Java is suited for a somewhat different
set of applications than a language like C, though of course there is
much overlap between the two.
We saw last time how the Java virtual machine model insulates a Java
program from hardware, and how this is both desirable and undesirable.
In this issue we'll talk a bit about verification, another aspect of
security. A java class is compiled into platform-independent ".class"
files containing byte streams. These streams contain the actual
program codes (bytecodes, constants, debugging information, and so on).
Suppose that I have the hello program:
public class hello { public static void main(String args[]) { System.out.println("Hello World"); } } |
and I compile it:
$ javac hello.java
resulting in a "hello.class" file of 461 bytes (in JDK 1.1). And I'm
feeling malicious and decide to tweak one of the bytes:
import java.io.*; public class tweak { public static void main(String args[]) { int offset = Integer.parseInt(args[0]); try { RandomAccessFile raf = new RandomAccessFile("hello.class", "rw"); raf.seek(offset); raf.writeByte(97); raf.close(); } catch (Throwable e) { System.err.println("*** exception ***"); } } } |
by saying:
$ javac tweak.java $ java tweak 0 |
thereby writing the byte "97" into location 0 in "hello.class".
Obviously, this is not how Java was intended to be used. What will
happen?
It turns out that the first four bytes of a Java .class file must be
the hex value "0xCAFEBABE", and writing 0 into one of these will
invalidate the file. If I then try to run the program:
$ java hello |
I will get an immediate error. This particular feature is fairly
common in program binaries and often goes by the name of "magic
number".
When a Java program is run, the interpreter first invokes the Java
Verifier. The Verifier checks the magic number along with other
properties of the .class file, including:
There are many other checks done by the Verifier on .class files.
This list is merely illustrative. Verifying a class not only improves
security, but speeds up actual bytecode interpretation because the
checks don't have to be repeated.
An interesting book that goes into detail on Java security is "Java
Security - Hostile Applets, Holes, and Antidotes", by Gary McGraw and
Edward Felten, published 1997 by Wiley for $20.
We will be discussing security further in future issues.
In previous issues we've looked at how the Java programming model
insulates one from hardware, and how various types of checks are
performed before executing a Java program.
In this issue we'll look at the Java security manager. This is a
class in java.lang, that can be used to impose a specific security
policy on running Java programs (including applets loaded by a Web
browser).
The SecurityManager class contains a set of methods, such as
checkRead(), that are called by Java core libraries. In a standalone
program, typically no security manager is installed, and the program
is wide open as to what it's allowed to do. An instance of a
SecurityManager class object can be installed exactly once, after
which the security policies in force are those of the class instance.
By default, nothing is allowed, and so a class derived from
SecurityManager needs to override some of the methods.
Let's see how this works in practice. There is a method:
public void checkRead(String file) { throw new SecurityException(); } |
found in SecurityManager. We can override this method as follows:
import java.io.*; class test_SecurityManager extends SecurityManager { public void checkRead(String file) { if (file.charAt(0) == '/') throw new SecurityException(); } } public class Security { public static void main(String args[]) { System.setSecurityManager(new test_SecurityManager()); try { FileInputStream fis = new FileInputStream("/xxx"); fis.read(); fis.close(); } catch (SecurityException e) { System.err.println("security exception"); } catch (IOException e) { System.err.println("I/O exception"); } catch (Throwable e) { System.err.println("some other exception"); } } } |
to check the pathnames of files that an application accesses. In this
example, we check that the pathname does not start with "/". If it
does, we throw a SecurityException.
As we said, a security manager can only be established once in a given
session (if this was not so, the old manager could simply be replaced
by a more liberal one). By default, the check methods disallow
operations, so an overriding method will typically relax some
restriction. A Web browser may or may not have mechanisms for
changing the default security policies it imposes on applets that are
downloaded and executed.
This particular example does not work correctly with JDK 1.1 running
on Windows 3.51, and requires JDK 1.1.1.
In previous issues we've looked at some of the aspects of Java
security, namely Java's use of an abstract machine removed somewhat
from the hardware, verification of loaded code, and the security
manager.
Another aspect of security centers around class loaders. A class
loader in Java is responsible for converting a stream of bytes (for
example, as found in a .class file) into a class known to the Java
runtime system. When you run a Java standalone program, there is a
built-in loader that converts .class files into a running program.
Similarly, when an applet is downloaded from the Web, there is a
loader responsible for installing it on your system.
Other languages such as C also have the notion of loading, where a
binary image is turned into an executing program. But with Java there
are some security aspects with loading as well.
For example, suppose that I have malicious intentions, and I decide to
fool the Java system by trying to override one of the core classes,
java.lang.Runtime, supplying my own version. How is this prevented by
the class loader?
One way this is done is by keeping local built-in classes distinct
from those loaded from a URL somewhere on the Web, that is, keeping
classes in separate name spaces, according to where they were loaded
from. When a class is looked up, the local name space can be checked
first.
There's more we could say about class loaders, but this brief
introduction gives an idea of what is going on behind the scenes.
Another type of security problem in Java, that's a bit different from
what we've already looked at in this series, is denial of service.
This involves an applet that meets the security requirements
previously discussed, but which essentially takes over the user's
whole machine and prevents anything else from being done. That is, I
as a user am surfing the Web, and come to a page with a Java applet
call embedded in the HTML. The applet starts up and prevents me from
using my computer for any other function.
A couple of examples of such applets would be one that does some sort
of number crunching function, or one that creates a large number of
very large windows. McGraw and Felten's "Java Security" book gives
some examples of such applets, which we won't reproduce here.
The simplest solution to this type of problem is to by default disable
your Web browser's ability to execute Java applets, and only
selectively enable this feature for Web sites that you trust. Another
possible longer-term approach would involve being able to establish
some sort of resource usage limits.