ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Java vs. .NET Security, Part 1
Security Configuration and Code Containment

by Denis Piliptchouk
11/26/2003

This article initiates a series that will provide a side-by-side technical comparison of security features of Java v1.4.2/J2EE v1.4 (beta2) and .NET v1.1 platforms. The following areas will be considered: Security Configuration and Code Containment (this article, Part 1), Cryptography and Communication (Part 2), Code Protection and Code Access Security, or CAS, (Part 3), and Authentication and User Access Security, or UAS, (Part 4).

This series is based on the research material that was used as a foundation of the feature article, "Securing .NET and Enterprise Java: Side by Side", which was written by Vincent Dovydaitis and me, and which appeared in Numbers 3-4 of Computer Security Journal in 2002.

It would be unrealistic to expect complete and detailed coverage of all aspects of platform security in the space available. Rather, this series attempts to highlight the most important issues and directions, leaving it up to the reader to check the appropriate manuals and reference literature.

Another important aspect of platform security is that it does not attempt to deal with all possible types of threats, thus requiring the services of an OS and third-party software (such as using IIS in .NET). These services and applications will also be outside of the scope of this publication.

Configuration

Configuration on both platforms is handled through XML or plain-text files, which can be modified in any text editor, or through the supplied tools. However, the platforms differ significantly in how they handle configuration hierarchies.

In the .NET world, Mscorcfg.msc and Caspol.exe can be used to modify all aspects of security configuration. The former provides a GUI interface, shown in Figure 1, to modify it.

Figure 1
Figure 1. Mscorcfg.msc screen

On the other hand, Caspol.exe provides a number of command-line options, appropriate for use in scripts and batch routines. Here's how it would be used to add full trust to an assembly: caspol -af MyApp.exe.

Java platform provides a single GUI-based tool, policytool.exe, shown in Figure 2, for setting code- and Principal-based security policies. This tool works with arbitrary policy files (as long as they are in the proper format), as opposed to .NET, where names and locations of the configuration files are fixed (see below).

Figure 2
Figure 2. Policytool.exe screen

.NET defines machine-wide and application-specific configuration files, and allows for enterprise, machine, and user security policy configuration, whose intersection provides the effective policy for the executing user. These file have fixed names and locations, most of them residing under the Common Library Runtime (CLR) tree, at: %CLR install path%\Config

For .NET v1.1, the location is: C:\WINNT\Microsoft.NET\Framework\v1.1.4322\CONFIG.

Multiple versions of CLR may co-exist on the same computer, but each particular version can have only a single installation. Since security policy files cannot be specified as runtime parameters of .NET applications, this centralized approach hurts co-existence when applications require conflicting policy settings.

Three security configuration files (enterprise, machine, and user) contain information about configured zones, trusted assemblies, permission classes, and so on. The general machine configuration file, on the other hand, contains machine-wide settings for algorithms, credentials, timeouts, and so on. Additionally, certain parameters (for instance, ASP.NET authentication/authorization parameters) can be configured or overriden in the application configuration file. The files' names and locations are listed below:

Core Java and J2EE configuration files have specific locations, but locations of additional configuration files for extension and J2EE products vary by vendor. J2SE does provide significant runtime flexibility by using a number of command-line application parameters, which allow the caller, on a per-application basis, to set keyfiles, trust policy, and extend the security policy in effect:

java -Djava.security.manager 
  -Djava.security.policy=Extend.policy Client1

or to completely replace it:

java -Djava.security.manager 
  -Djava.security.policy==Replace.policy Client2

The Java platform's specifications require the following configuration files:

Certain JVM parameters may be configured only in $JAVA_HOME/lib/security/java.security, as shown in the examples below:

Note: By allowing command-line JVM parameters, Java provides a significantly more flexible and configurable environment, without conflicts among multiple JVM installations.

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:

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:

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:

.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:

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.

Copyright © 2009 O'Reilly Media, Inc.