ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Java vs. .NET Security, Part 1
Pages: 1, 2

Code Containment: Verification

In both environments, the respective VM starts out with bytecode, which it verifies and executes. The bytecode format is well known and can be easily checked for potential violations, either at loading or at execution time. Some of the checks include stack integrity, overflow and underflow, validity of bytecode structure, parameters' types and values, proper object initialization before usage, assignment semantics, array bounds, type conversions, and accessibility policies.



Both Java and CLS languages possess memory- (or type-) safety property; that is, applications written in those languages are verifiably safe, if they do not use unsafe constructs (like calling into unmanaged code).

In .NET, CLR always executes natively compiled code; it never interprets it. Before IL is compiled to native code, it is subjected to validation and verification steps. The first step checks the overall file structure and code integrity. The second performs a series of extensive checks for memory safety, involving stack tracing, data-flow analysis, type checks, and so on. No verification is performed at runtime, but the Virtual Execution System (VES) is responsible for runtime checks that type signatures for methods are correct, and valid operations are performed on types, including array bounds checking. These runtime checks are accomplished by inserting additional code in the executing application, which is responsible for handling error conditions and raising appropriate exceptions. By default, verification is always turned on, unless SkipVerification permission is granted to the code.

The Java VM is responsible for loading, linking, verifying, and executing Java classes. In the HotSpot JVM, Java classes are always interpreted first, and then only certain, most frequently used sections of code are compiled and optimized. Thus, the level of security available with interpreted execution is preserved. Even for compiled and optimized code, the JVM maintains two call stacks, preserving original bytecode information. It uses the bytecode stack to perform runtime security checks and verifications, like proper variable assignments, certain type casts, and array bounds; that is, those checks that cannot be deduced from static analysis of Java bytecode.

Code verification in a JVM is a four-step process. It starts by looking at the overall class file format to check for specific tags, and ends up verifying opcodes and method arguments. The final pass is not performed until method invocation, and it verifies member access policies. By default, the last step of verification is run only on remotely loaded classes. The following switches can be passed to JVM to control verification:

  • -verifyremote: verifies only classes from the network (default)
  • -verify: verifies all classes
  • -noverify: turns off verification completely

Starting with the initial releases of Java, there have been multiple verification problems reported, where invalid/malicious bytecode could sneak beyond the verifier. At the moment, there are no new reports about verification bugs, and Java 2 documentation does not list verification switches, which implies that the verification is always run in full.

However, the -verify switch is still required for local code to behave correctly, as the following example shows. Given class Intruder...

public class Intruder
{
   public static void main(String[] args)
   {
      Victim v = new Victim();
      System.out.println(
        "Intruder: calling victim's assault() method...");
      v.assault();
   }
}

A Victim class with a public method:

public class Victim
{
   public void assault()
   {
      System.out.println(
        "Victim: OK to access public method");
   }
}

And another version of the Victim class with a private method:

public class Victim
{
   private void assault()
   {
      System.out.println(
        "Victim: Private method assaulted!!!");
   }
}

We get the following output when we run a script to compile and run Intruder first against the public version of Victim, and then, without recompiling the Intruder class, against the private verison. Finally, it is run against the private version again, this time with -verify passed as a command-line argument to JVM:

********************************************
* Calling public version of Victim.assault()
********************************************
Intruder: calling victim's assault() method...
Victim: OK to access public method
*********************************************
* Calling private version of Victim.assault()
*********************************************
Intruder: calling victim's assault() method...
Victim: Private method assaulted!!!
****************************************************
* Calling private Victim.assault() with verification
****************************************************
Intruder: calling victim's assault() method...
java.lang.IllegalAccessError:
 tried to access method Victim.assault()V from class Intruder
	at Intruder.main(Intruder.java:7)
Exception in thread "main" 

The sources and the execute.bat file are available as a zip file for download.

Note: JVM, as opposed to .NET, does not verify local code by default. On the other hand, JVM always preserves the bytecode stack for runtime checks, while .NET relies on a combination of static analysis and injection of verification code at runtime.

Code Containment: Application Isolation

In effect, each VM represents a mini OS by replicating many of its essential features. Each platform provides application isolation for managed applications running side by side in the same VM, just as OSes do it. Automatic memory management is an important feature of both environments--it aids tremendously in writing stable, leak-free applications. The "CAS" section in Part 3 of this series will provide detailed discussion about permissions, policies, and access checks.

Both environments do allow for exercising unsafe operations (JNI in Java; unsafe code and P/Invoke in .NET), but their use requires granting highly privileged code permissions.

Application Domains (AppDomains) represent separate .NET applications running inside the same CLR process. Domain isolation is based on the memory safety property because applications from different domains cannot directly access each other's address spaces, and they have to use the .NET Remoting infrastructure for communication.

Application security settings are determined by CLR on a per-domain basis, by default using host's security settings to determine those for loaded assemblies. The CLR receives information about the assembly's evidence from so-called trusted hosts:

  • Browser host (Internet Explorer): Runs code within the context of a web site.
  • Server host (ASP.NET): Runs code that handles requests submitted to a server.
  • Shell host: Launches managed applications (.exe files) from the Windows shell.
  • Custom hosts: An application that starts execution of CLR engine.

Domain security settings can be administered only programmatically; that is, there is no configuration file where those could be set. If the host process is granted a special SecurityPermission to control evidence, it is allowed to specify the AppDomain's policy at creation time. However, it can only reduce the compound set of permissions granted by the enterprise, machine, and user policies from security policy files. The following example, taken from MSDN documentation, illustrates using programmatic AppDomain policy administration to restrict permission set of the new domain to Execution only:

using System;
using System.Threading;
using System.Security;
using System.Security.Policy;
using System.Security.Permissions;

namespace AppDomainSnippets
{
   class ADSetAppDomainPolicy
   {
      static void Main(string[] args)
      {
         // Create a new application domain.
         AppDomain domain =
            System.AppDomain.CreateDomain("MyDomain");
         
         // Create a new AppDomain PolicyLevel.
         PolicyLevel polLevel =
            PolicyLevel.CreateAppDomainLevel();
         // Create a new, empty permission set.
         PermissionSet permSet =
            new PermissionSet(PermissionState.None);
         // Add permission to execute code to the
         // permission set.
         permSet.AddPermission
            (new SecurityPermission(
               SecurityPermissionFlag.Execution));
         // Give the policy level's root code group a
         // new policy statement based on the new
         // permission set.
         polLevel.RootCodeGroup.PolicyStatement =
            new PolicyStatement(permSet);
         // Give the new policy level to the
         // application domain.
         domain.SetAppDomainPolicy(polLevel);
         
         // Try to execute the assembly.
         try
         {
            // This will throw a PolicyException
            // if the executable tries to access
            // any resources like file I/O or tries
            // to create a window.
            domain.ExecuteAssembly(
               "Assemblies\\MyWindowsExe.exe");
         }
         catch(PolicyException e)
         {
            Console.WriteLine("PolicyException: {0}",
                              e.Message);
         }

         AppDomain.Unload(domain);
      }
   }
}

Application-style isolation is achieved in Java through a rather complicated combination of ClassLoaders and ProtectionDomains. The latter associates CodeSource (i.e., URL and code signers) with fixed sets of permissions, and is created by the appropriate class loaders (URL, RMI, custom). These domains may be created on demand to account for dynamic policies, provided by JAAS mechanism (to be covered in Part 4, in the "Authentication" section). Classes in different domains belong to separate namespaces, even if they have the same package names, and are prevented from communicating within the JVM space, thus isolating trusted programs from the untrusted ones. This measure works to preserve and prevent bogus code from being added to packages.

Secure class loading is the cornerstone of JVM security--a class loader is authorized to make decisions about which classes in which packages can be loaded, define its CodeSource, and even set any permissions of its choice. Consider the following implementation of ClassLoader, which undermines all of the access control settings provided by the policy:

protected PermissionCollection
    getPermissions (CodeSource src) {
	PermissionCollection coll =
	    new Permissions();
	coll.add(new AllPermission());
	return coll;
}

Note: .NET's AppDomains, which are modeled as processes in an OS, are more straightforward and easier to use than Java's ProtectionDomains.

Code Containment: Language Features

Both platforms' languages have the following security features:

  • Strong typing (a.k.a. statically computable property): all objects have a runtime type. There is no void type--a single-root class hierarchy exists, with all classes deriving implicitly from Object root.

  • No direct memory access: therefore, it is impossible to penetrate other applications' memory space from managed code.

  • Accessibility/const modifiers (such as private/protected/public): The const (final) modifier's semantics, however, is quite different from that in C++; only the reference is constant, but the object's contents can be freely changed.

  • Default objects initialization: "zero initialization" for heap-allocated objects. Proper initialization of stack objects is checked by the VM at runtime.

  • Choice of serialization and transient options: controls contents of serialized objects that are outside of VM protection domains.

  • Explicit coercion required: there are few well-defined cases when implicit coercion is used. In all other cases (and with custom objects) explicit conversion is required.

.NET defines the following accessibility modifiers: public, internal (only for the current assembly), protected, protected internal (union of protected and internal), and private. All properties are defined via Getters/Setters, and access to them is controlled at runtime by CLR.

In C#, there are two choices for declaring constant data values: const for built-in value types, whose value is known at compile time); and readonly for all others, whose value is set once at creation time:

public const string Name = "Const Example";
//to be set in the constructor
public readonly CustomObject readonlyObject;

A .NET class can be marked as serializable by specifying [Serializable] attribute on the class. By default, its full state is stored, including private information. If this is not desirable, a member can be excluded by specifying a NonSerialized attribute, or by implementing a ISerializable interface to control the serialization process.

[Serializable]
public struct Data
{
  //Ok to serialize this information
  private string publicData;
  //this member is not going to be serialized
  [NonSerialized] private string privateData;
}

Java language provides the following features to support writing secure applications:

  • Accessibility modifiers: public, protected, package protected, private.
  • Final classes and methods: final keyword can be applied to a class, method, or variable, and means that this entity cannot be changed or overridden.
  • Serialization and transient options: for classes implementing a marker Serializable interface, the serialized object includes private members as well, unless they are decorated as static or transient. Use the readObject/writeObject pair to control the content of a serialized object. Alternatively, implementing the Externalizable interface's methods readExternal/writeExternal gives you complete control over the serialization process.
public class Person implements Serializable
{
  //get serialized by default
  private string name, address;
  //excluded from the default serialization
  transient int salary;
};

Note: In terms of protective language features, both platforms rate approximately equal, with .NET having a slight edge due to higher flexibility when it comes to constant modifiers.

Conclusions

This article covered security configuration issues and different aspects of code containment on .NET and Java platforms. Java offers a lot of advantages with its configurability. When it comes to code containment, both platforms have pretty strong offerings, with .NET having slightly more choices and being more straightforward to use.

The next article of this series, Part 2, will cover cryptography and communication-protection mechanisms on the Java and .NET platforms.

Denis Piliptchouk is a senior technical member of BEA's AquaLogic Enterprise Security group, participates in OASIS WSS and WS-I BSP standards committees, and regularly contributes to industry publications.


Return to ONJava.com.