Code Access Security

Code Access Security represents a fundamentally different way of controlling access rights to protected resources. Traditionally, permissions such as access to the file system, databases or network were allocated based on characteristics of the user. All processes executed by the user would assume an equivalent set of permissions.

However, models of development such as the growth in code mobility and components have deemed this inadequate. It became apparent that the actions of untrusted or remote components should be restricted more than trusted code, thus turning the permission granting process on its head. Instead of looking at user characteristics, Code Access Security looks at characteristics of the code, including its trusted status when deciding on how to grant permissions. The details of how this takes place and which permissions are granted are configurable by the system administrator.

As a result of this flexibility, Code Access Security is a complex topic. Though there are many articles that describe it in a lot of detail, it's easy to end up confused by the myriad of terms - permissions, permission sets, evidence, code groups, membership conditions and policy levels are just a few of the concepts to understand. I found it was easier to look at the topic from two perspectives - that of the systems administrator and the developer. Though appreciation of the whole is important, most of the complexity is only relevant depending on which group you fall into.

Administrators perspective

A systems administrator is responsible for a number of machines upon which users might attempt to run code. Within the organisation there will be unique concerns about how much flexibility this code should have; should arbitrary Internet downloads be allowed to write to the hard disk, or access private databases within the enterprise? The answer is probably no. Many permissions however will be deemed acceptable depending on the needs of the organisation. The administrator is responsible for defining a security policy to represent choices such as these. At a high level, the administrator needs to ask themselves the following two questions:

1. How should code be grouped?

Should code running from the local network be granted different permissions to code running from the Internet or local host? If so, we have to conceptually divide these types of code into separate groups.

Likewise, if some code has been digitally signed, the question must be asked if this should be placed into one 'signed code' group, or whether this group should be further subdivided into all of the code signed by Microsoft, and all of the Code signed by Acme corp? Again this choice is likely to be made based on whether we need to assign different permissions to code from the two groups.

2. Which permissions should be granted to code groups?

For every code group, which permissions should be granted to code in that group? Does one code group, such as the Microsoft signed group running from the Internet deserve a greater level of permissions than unsigned code running from the local host?

Every code group has an associated permission set. The administrator decides exactly how code is deemed to have fallen into a specified code group, and which permissions it should be granted as a result of this classification.

Evidence

Every assembly can be deemed as presenting a set of evidence. Evidence includes any public keys, the presence of a hash, the source directory and the zone.

Code groups

As stated, code can be grouped together if we decide to allocate all instances the same permission set.

The .NET framework adds the concept of code groups. Every code group has a set of membership conditions. These are the conditions that must be satisfied if a piece of code is to become a member of the group. For instance, the default 'Microsoft Strong Name' group has the membership condition that an assembly must have an attached strong name signed with the public key of Microsoft. Only presented assemblies that meet this condition will become a member of this group. It is through the examination of the aforementioned evidence that the membership conditions are examined. For this reason, evidence is an important concept for both administrators and developers.

There are 12 code groups defined in a default installation of the .NET framework, with these defaults being based on the concept of security zones found in Internet Explorer. Each provides a slightly different permission set; the Internet code group provides a very restrictive permission set and the aforementioned 'Microsoft Strong Name' group will provide a much more flexible permission set.

Note that a given assembly can be part of more than one code group. The final grant will be the union of all of the permissions granted from each group membership.

Policy levels

Administrators can configure security policy across three levels - these are the enterprise, the machine and the user. This layered system enables certain policies to be implemented across the entire enterprise, such as a complete ban on writing to the file system. Further policy decisions can then be made for specific machines and users. For instance, machines within a given department might be restricted from accessing computers outside of a certain IP address range, and a specific subset of users might be prevented from running any programs from the Internet that attempt to display any dialogs. Notice that each policy level adds further restrictions, but we can make these restrictions in a finely grained and flexible way.

Policy is stored in XML files, one for each policy level. The location of these files is listed here. It is usual to administer security policy using the command line caspol.exe, or the graphical mscorcfg.cfg.

Developers perspective

The second perspective is that of the application developer targeting the .NET platform. The developer should not care about the process of how policy files are manipulated, and how the runtime uses them to decide on which permissions to grant code. Though some understanding is useful, for the most part developers should approach the problem as though their assembly will be granted a permission set that is completely out of their control. Fortunately, Code Access Security allows the developer to elegantly handle situations where a required permission is not granted, or an unrequired permission is granted.

Evidence

Evidence is also relevant to the developer. Intuitively, adding evidence such as a hash or Strong Name is likely to result in a greater permission set as it represents a greater amount of assurance the assembly is safe. For instance, many developers have the problem that their code will not carry out certain operations until they strongly name their assemblies.

Permissions

The permission classes are the most important element from the perspective of the developer. Each protected action, such as file system, network, DNS and printer access is represented as a permission class in the .NET framework class library. Permissions can be instantiated and configured to represent the idiosyncracies of the resource. For instance, the permission class representing the File system, FileIOPermission can be created and have a set of readable and writable paths added to it. It is then possible to specifically request that the permission is allowed by the runtime, not only for the component in question, but also for all direct and indirect callers:

Note that these demands and denials are present in the .NET framework base classes. For example, constructing an instance of the System.Net.Sockets.Socket class, and calling the Connect() method produces a demand for the SocketPermission.

RequestMinimal, RequestOptional and RequestDeny

Using attributes, the developer can specifically request a permission set that their assembly needs or does not need. When the runtime attempts to load the assembly, these requests are considered. Any failures to grant the requested permissions can be handled at the outset.

More importantly, the developer can explicitly deny specific permissions. This allows the runtime to reduce the subset of granted permissions to a component, reducing the scope that someone can misuse the component in a way other than that intended by the original developer. In the code below, the developer is stating that even if hard disk write permission is granted, then accessing the Windows folder should be denied. This prevents an untrusted, malicious component from luring the potentially more trusted component into performing access to the Windows folder on it's behalf. This blanket denial would prevent a multitude of security holes that might have been presented by the component.

Requesting permissions of inheritors and linkersA developer may like to state that any component that inherits, or links to their component has to possess certain permissions. For instance, if a developer has designed a class that wraps DNS access, they may like to ensure that any inheritance of the component also has permission to access the DNS resources - it would be of little use otherwise.

Elegant handling of permission deficits If a permission is demanded, but not granted, then a SecurityException is thrown. Catching these exceptions means that the developer can handle the problem elegantly, either by displaying an error message or attempting an alternate course of action.

Summary

Administrators define local security policy by defining code groups. They indicate which membership conditions have to be met in order for code to fall into the group, and which permissions the assembly should be granted once group membership has been established. It is the responsibility of the runtime to examine the evidence of a presented assembly, and determine which membership conditions are met. The final grant set is calculated by summing together all of the permissions granted by the individual group memberships.

Much of the complexity of Code Access Security is not relevant for the developer. The developer can specifically request or deny permissions at the level of the assembly, with these being considered at assembly load time. At run time, further demands can be made, in addition to temporary denials that restrict permissions prior to calling untrusted code. The .NET class libraries make demands, with each demand ensuring that all items on the call stack contain the requested permission. A developer can also request the libraries linking to or inheriting from their code possesses a certain permission set.