Secure thread collaboration across protection domains

Build solid applications with the AccessControlContext and the GuardedObject classes

Let's consider two related but slightly different scenarios.

  1. You're building an application with two threads, a server and a client,at its heart. The design requirements permit the server thread to access every file in the filesystem. However, they forbid the client thread from accessing files outside a small part of the filesystem. The server API defines a mechanism whereby a client thread can request the server thread to perform a sensitive operation on an arbitrary file. How do you ensure that the server thread carries out this operation only on files that the client can legitimately access?
  2. Your application's threads consist of a producer and a consumer. The design requirements are the same as above -- the producer is the server, and the consumer is the client. When requested by the consumer thread, the producer thread creates a filesystem object, such as an instance of the FileInputStream class, and hands it to the consumer. The consumer can immediately use the resource, hold on to it, or hand it to another consumer. How do you ensure that the consumer thread doesn't use resources it should not have access to?

When confronted with situations like the two outlined above, you should develop solutions that take advantage of the security features provided by the Java programming language and class library. This will save you work and prevent you from making mistakes. This month I'll take you on a tour of the AccessControlContext and the GuardedObject classes, two tools that will help you solve problems like those above.

Java security in a couple of paragraphs

Although the two preceding problems differ slightly, both share a common characteristic. They both involve making security-related decisions outside the bounds of Java's standard security machinery.

In order to understand what that means, it's worth taking a few moments to learn about Java security. Let's dive in.

Every method is part of a class. Every class is associated with a protection domain. A protection domain is a collection of classes that share critical security-related features -- they originate from the same place, and the same entity signs them. Every protection domain is associated with a set of permissions that define the security-sensitive operations that classes in the protection domain can perform.

That makes up only part of the picture. The decision whether or not to allow a security-sensitive operation to proceed depends not only on the permissions held by the current method but also on the permissions held by the method that called the current method, and by the method that called the method that called the current method, and so on, all the way back to the first method invocation by the Java Virtual Machine (VM).

Figure 1. A thread's execution context

If you imagine the chain of method calls as a stack of blocks, it would look something like Figure 1, which I refer to as the execution context. Every thread has its own execution context.

By examining the execution context, the AccessController class decides whether or not to allow a method to execute a piece of security-critical code. More specifically, it makes the determination by examining the protection domains associated with the methods in the execution context. The AccessController class permits the operation if and only if every protection domain in the current execution context holds the appropriate permission. This "principle of least privilege" is a key characteristic of the Java security model.

Security at the extremes

Let's consider again the two scenarios described above. Both situations present two threads, and therefore two contexts. The two threads will likely execute code from different protection domains as well. For example, in Situation 1, the server thread must belong to a protection domain with permission to access all files in the filesystem, while the client thread must belong to a protection domain with a more restrictive set of permissions.

The Java security model provides two mechanism for dealing with situations like those presented in the introduction: the AccessControlContext class and the GuardedObject class.

Let's look at each and examine how they solve these security problems.

Class AccessControlContext

The AccessControlContext class encapsulates a static snapshot of an execution context. The getContext() method on the AccessController class creates an AccessControlContext instance for the current execution context.

The following code illustrates how to use AccessControlContext in the context of Situation 1.

public
static
class Client
implements Runnable
{
    private
    Server m_server = null;
    public
    void
    link(Server server)
    {
      if (m_server != server)
      {
        m_server = server;
        m_server.link(this);
      }
    }
    public
    void
    run()
    {
      for (int i = 0; i < 10; i++)
      {
        Server.Message message = new Server.Message("/tmp/out.tmp", Integer.toString(i) + '\n');
        m_server.logMessage(message);
      }
    }
  }
  public
  static
  class Server
  implements Runnable
  {
    private
    Client m_client = null;
    public
    void
    link(Client client)
    {
      if (m_client != client)
      {
        m_client = client;
        m_client.link(this);
      }
    }
    private
    LinkedList m_linkedlist = new LinkedList();
    public
    synchronized
    void
    logMessage(Message message)
    {
      message.m_accesscontrolcontext = AccessController.getContext();
      m_linkedlist.add(message);
      notifyAll();
    }
    private
    synchronized
    Message
    retrieveMessage()
    throws InterruptedException
    {
      while (m_linkedlist.isEmpty()) wait();
      return (Message)m_linkedlist.removeFirst();
    }
    public
    void
    run()
    {
      while (true)
      {
        Message message = null;
        try
        {
          message = retrieveMessage();
        }
        catch (InterruptedException interruptedexception)
        {
          break;
        }
        final String stringDestination = message.m_stringDestination;
        final String stringMessage = message.m_stringMessage;
        AccessController.doPrivileged
        (
          new PrivilegedAction()
          {
            public
            Object
            run()
            {
              FileWriter filewriter = null;
              try
              {
                filewriter = new FileWriter(stringDestination, true);
                filewriter.write(stringMessage);
                filewriter.close();
              }
              catch (IOException ioexception)
              {
                ioexception.printStackTrace();
              }
              return null;
            }
          },
          message.m_accesscontrolcontext
        );
      }
    }
    public
    static
    class Message
    {
      private
      AccessControlContext m_accesscontrolcontext = null;
      private
      String m_stringDestination = null;
      private
      String m_stringMessage = null;
      public
      Message(String stringDestination, String stringMessage)
      {
        m_stringDestination = stringDestination;
        m_stringMessage = stringMessage;
      }
    }
  }

The preceding code contains two threads, identified as the client and the server. The server thread writes messages to logs, which correspond to files in the local filesystem -- a log is specified by its path and filename. The requirements state that there are many logs, some of which the client thread can write to and some of which it can't.

Along with its request to write the message, the client passes its execution context in the form of an AccessControlContext instance. It obtains this instance by calling the getContext() method.

The server thread obtains the request from its queue. Before attempting to write to the log, it first checks the permissions associated with the protection domains of the client by invoking the doPriviledged() method on the AccessController class. This method accepts an instance of the AccessControlContext class in addition to a block of code containing the security-sensitive operation.

Figure 2 illustrates how this works.

Figure 2. Permission resolution using an AccessControlContext instance

The arrows illustrate the path followed by the AccessController class as it decides whether or not to permit the operation. Beginning at the point where the security-critical operation is attempted, the AccessController class examines each protection domain for the required permission. When it reaches the point at which the doPriviledged() method was called, it stops checking the current execution context and instead continues checking the client's execution context. In this way, it considers the client's permissions when deciding whether or not to permit a security-sensitive operation.

Class GuardedObject

An instance of the GuardedObject class "guards" a wrapped instance of another class from code that lacks the correct permissions.

Figure 3 illustrates the relationship between the GuardedObjectinstance, the Guard instance, and the wrapped object.

Figure 3. The GuardedObject and Guard instances

The following code illustrates how to use a GuardedObject instance and an associated Guard instance in the context of Situation 2.

  public
  static
  class Consumer
  implements Runnable
  {
    private
    Producer m_producer = null;
    public
    void
    link(Producer producer)
   {
      if (m_producer != producer)
      {
        m_producer = producer;
        m_producer.link(this);
      }
    }
    public
    void
    run()
    {
      for (int i = 0; i < 10; i++)
      {
        Object object = m_producer.retrieveLocation().getObject();
      }
    }
  }
  public
  static
  class Producer
  implements Runnable
  {
    private
    Consumer m_consumer = null;
    public
    void
    link(Consumer consumer)
    {
      if (m_consumer != consumer)
      {
        m_consumer = consumer;
        m_consumer.link(this);
      }
    }
    private
    String m_stringLocation = null;
    public
    synchronized
    GuardedObject
    retrieveLocation()
    {
      String stringLocation = null;
      try
      {
        while (m_stringLocation == null) wait();
        stringLocation = m_stringLocation;
        m_stringLocation = null;
        notifyAll();
      }
      catch (InterruptedException interruprtedexception)
      {
      }
      try
      {
        GuardedObject guardedobject = new GuardedObject
        (
          new FileInputStream(stringLocation),
          new FilePermission(stringLocation, "read")
        );
        return guardedobject;
      }
      catch (IOException ioexception)
      {
        ioexception.printStackTrace();
      }
      return null;
    }
    private
    synchronized
    void
    storeLocation(String stringLocation)
    throws InterruptedException
    {
      while (m_stringLocation != null) wait();
      m_stringLocation = stringLocation;
      notifyAll();
    }
    public
    void
    run()
    {
      while (true)
      {
        try
        {
          storeLocation("/tmp/out.tmp");
        }
        catch (InterruptedException interruptedexception)
        {
          break;
        }
      }
    }
  }
}

The preceding code contains two threads identified as the producer and the consumer. In response to consumer requests, the producer thread creates a FileInputStream instance, wraps it in an instance of the GuardedObject class, and returns it to the consumer.

Associated with every instance of the GuardedObject class is a Guard instance. The Guard interface has only one method, the checkGuard() method. checkGuard() should make the appropriate check and throw a SecurityException if access to the guarded object is not allowed.

In creating the GuardedObject instance, I used an instance of the FilePermission class as the guard. The FilePermission class, and all classes that subclass the Permission class, implement the Guard interface and are therefore suitable for use as guards. The code>Permission class implements the checkGuard() method as follows:

  public
  void
  checkGuard(Object object)
  throws SecurityException
  {
    SecurityManager securitymanager = System.getSecurityManager();
    if (securitymanager != null)
    {
      securitymanager.checkPermission(this);
    }
  }

That is the same logic used to implement normal security checks in the Java class libraries.

Conclusion

When reflecting on the use of the AccessControlContext and the GuardedObject classes, it's important to consider the benefits they ring to the table. Superficially, they allow you to solve a pair of challenging programming tasks. Perhaps more importantly, they allow you to solve these tasks using tools that already exist and that have already undergone public scrutiny and test. When building secure applications, this type of conservative strategy is often the best bet.

Todd Sundsted has been writing programs since computers became available in convenient desktop models. Though originally interested in building distributed applications in C++, Todd moved on to the Java programming language when it became the obvious choice for that sort of thing. In addition to writing, Todd is chief architect and cofounder of PointFire.
1 2 Page 1
Notice to our Readers
We're now using social media to take your comments and feedback. Learn more about this here.