Registro | Ayuda


Security Mechanisms of the .NET Framework Platform

Fecha de publicación: 2008.04.09
Grado de Dificultad: 3

While developing an IT system, or just an application for the .NET Framework platform, one is likely to face the task of implementing mechanisms of security, permissions, authentication and authorisation. This article describes some aspects of such usage.

While developing an IT system, or just an application for the .NET Framework platform, one is likely to face the task of implementing mechanisms of security, permissions, authentication and authorisation.

At that point there are at least three possible approaches to choose from:

  • brush the problem off or ignore it altogether,

  • implement a custom security model,

  • take advantage of security mechanisms built into .NET Framework

For obvious reason, the first approach is hardly a good one. The second one might be acceptable, then again it will require large amounts of time and effort. Last but not least, the third one usually turns out to be the best one… why?

An integrated security system

The .NET Framework platform contains an integrated security system providing administrators, programmers and end users with a consistent, uniform and transparent security model, all the necessary administrative tools and documentation. One might say - why bother with all that? After all our company is already protected by access passwords, firewalls, anti-virus scanners etc. However, one should bear in mind that in the great majority of cases the weakest link in the security chain is humans themselves. Quite frequently users use resources coming from many unknown sources - e.g. documents, e-mail attachments, Web sites or FTP. Such resources can contain malicious code or flaws which make them useful in attacks against, destruction of or theft of data. The security system in .NET Framework allows setting restrictions on application permissions depending on the trustworthiness of their source of origin. By default, every application coming from the local computer possesses a full set of permissions (Full Trust) which grants them access to all the resources of the computer. On the other hand, applications coming e.g. from the LAN or from the Internet have their permissions restricted from the beginning. Of course .NET Framework provides the means of flexible configuration of which sources of origin are trusted and to what degree, as well as to grant permissions to applications coming from particular sources. Implementation of mechanisms of authorisation and authentication that enforce presentation of user credentials in an application (e.g. by providing a login and a password) and restrict access to various features depending on the roles assigned to the given user, is important too. Here assistance is given by the platform itself, through the ability to control permissions depending on roles (Role-Based Security), together with mechanisms of identification and identity.

Security Policies

A security policy is the basis and the foundation of all the security mechanisms built into .NET Framework. Security policies are configurable and extensible definitions of conditions, basing on which the CLR grants appropriate permissions to every loaded assembly. Security policies can be set and modified by the administrator through the .NET Framework configuration tool - mscorcfg.msc (a snap-in for the management console MMC) - and the tool called caspol.exe (available from the command line).

A security policy consists of four main elements:

  • Security Policy Levels

  • Code Groups

  • Named Permission Sets

  • Permissions

In order to understand exactly the purpose and significance of these elements, as well as the dependencies between them, let us begin with the last one:

Permissions - that is, definitions of permissions which the CLR can grant loaded assemblies which match conditions defined by the security policy.

Permissions can be segregated into three groups:

  • Code Access Permissions

  • Identity Permissions

  • Role-Based Security Permissions

Code access permissions - they allow protection of resources and access to restricted operations against unauthorised use by untrusted assemblies. .NET Framework provides a series of built-in access permissions to files, the registry, the user interface, calls of non-managed code etc., thus making it possible for the administrator to restrict any assembly's access to various resources of the computer. The programmer on the other hand is able to implement explicit dropping of permissions the application doesn't require but which have been granted to it. Such approach allows one to limit permissions possessed by the application to the minimum that is necessary for its proper operations - which in turn decreases the risk of the application being exploited by malicious code (a virus, a trojan etc.) through flaws in its code in order to attack the machine the code has been run on. Additionally, the programmer is capable to simulate lack of permissions to access certain resources on his own computer, thus verifying behaviour of the application under such circumstances. In other words, the programmer is capable of testing the application in conditions more closely resembling its deployment environment.

Identity permissions - they are granted on the basis of Evidence, which may consist of various information about the origin of the given assembly, e.g. about the website the assembly has come from, who published it, does it possess a so-called Strong Name or which zone it comes from (Internet, Intranet, Local computer etc.). We can distinguish two types of evidence: Host Evidence and Assembly Evidence. Host evidence is created and provided when the given assembly is loaded by the so-called Host, that is e.g. the shell of Windows (Shell Host), a Web browser (Browser Host) or a Web server (Server Host). Assembly evidence is created and issued by the assembly itself (assuming appropriate functionality has been implemented and included) and by default is ignored - still, it can be used by programmers extending features of security policies provided by the platform.

Role-based security permissions - this permission (there is only one) is granted depending on current identity of the user of the application and the roles (s)he is assigned. Therefore, it makes it possible to restrict, at several levels (the whole assembly, methods, properties etc.), access to selected parts of code depending on the name of currently-authenticated user or the roles this user has been assigned.

Programmers can also create their own, custom permissions in either of the three aforementioned groups

Named Permission Sets

Named permission sets are exactly what their name says - named sets of code access permissions described a moment ago. In addition to the possibility for the administrators to define their own permission sets, the platform provides seven predefined, ready-to-use sets: FullTrust (unrestricted access to all the resources), SkipVerification (permissions to skip verification of code), Execution (execution permissions), Nothing (no permissions), LocalInternet (default permissions granted to applications on a local intranet), Internet (default permissions granted to Internet applications), Everything (unrestricted access to all the resources protected by built-in permissions

Code Groups

Code groups are tree-like structures containing membership conditions, having met which for a certain group results in grant of a permission set assigned to this group. Let us now attempt to resolve this enigmatic definition.

As mentioned earlier on, every loaded assembly gets created so-called Evidence, containing information about origin of that assembly. Afterwards, the CLR initiates the process of evaluating that evidence and, basing on this, granting the assembly appropriate permissions. Code groups are exactly what enables configuration and control of the evidence evaluation process. A single code group contains two very important properties: a membership condition and a permission set. The first property - the membership condition - can be e.g. the condition that an assembly must originate from a certain URL (e.g. "http://www.t3.com.pl") or possess a so-called Strong Name, in further section of this article we will describe other predefined kinds of condition provided together with .NET Framework. A test of meeting a condition takes place by comparing arguments of the condition with elements of the given assembly's evidence. The second property is a permission set - that is, permissions which will be granted if the membership condition is met. Therefore, now we know what a code group is - a definition of a condition of belonging to it and permissions granted when that condition is met. What is then the point of merging such code groups into a tree-like structure? The reason for that is that such hierarchical system of conditions and permissions allows the administrator to perform extremely flexible modelling of the permission-granting process. The root of such a tree is a code group (called "All_Code") whose membership condition is always met by all assemblies. All child code groups can be composed into arbitrary tree-like structures, in which the CLR will verify whether the condition is met for a certain code group assuming the conditions it its parent groups have been met as well. In order to understand better how code groups work, let us have a look at Figure 1.

Figure 1. Security policies - an example code group tree

The code group model presented in the figure tells the CLR that:

  • by default we revoke all assemblies all their permissions

  • however, if they come from a local intranet we grant them the permission set called "LocalIntranet"

  • if they originate from the trusted Web site "http:// t3.com.pl" we grant them "LocalIntranet" permissions too

  • moreover, if assemblies originate from the trusted Web site "http://t3.com.pl" and additionally possess a Strong Name, the public key of which matches the value provided as an argument to the condition - we grant it two permission sets: "LocalIntranet" and "Execution"

  • if an assembly originates from the trusted Web site "http://t3.com.pl" and displays a publisher certificate of the company (nomen omen) "Software", it will be granted "LocalIntranet" and "FullTrust" permissions

The final permission set is a sum of all the permissions the CLR obtained while traversing the tree. Therefore, if we launched an imaginary application originating from the Web site "http://t3.com.pl" and possessing a Strong Name the public key of which equals the value passed as an argument of the condition, such an application would be granted permissions defined by the permission set "LocalIntranet" along with permission contained in the permission set "Execution".

.NET Framework provides eight predefined membership conditions we can use:

  • All code - this condition is met by all assemblies

  • Application directory - this condition is met if the assembly in question is located in the same directory as the application or in one of its subdirectories

  • Cryptographic hash - this condition is met for assemblies whose MD5 or SHA1 has equals the one passed as an argument to the condition

  • Software publisher - this condition is met if the assembly in question is signed with a certificate issued by the publisher specified as an argument to the condition

  • Site membership - this condition is met if the assembly in question originates from a defined Web site

  • Strong name - this condition is met if the assembly possesses a Strong Name with its public key equal to the one passed to the condition as an argument

  • URL - this condition is met if the assembly originates from the URL passed as a condition argument ("*" wildcards are allowed)

  • Zone - this condition is met if the assembly originates from the zone passed to the condition as an argument (the following zones exist: Internet, Intranet, MyComputer, NoZone, Trusted, Untrusted).

Additionally, programmers are able to define their own, custom membership conditions - the same applies to permission sets and code groups. Furthermore, their definitions can be written to or read from XML files.

Security Policy Levels

Let us now move on to the most general element of a security policy, namely - to security policy levels.

We distinguish four policy levels:

  • Enterprise Policy, stored in the file enterprisesec.config

  • Machine Policy, stored in the file security.config

  • User Policy, stored in the file security.config in user's personal directory

  • Application Domain Policy

Each of these elements (except application domain policy) has its own tree of code groups, list of permission sets, definitions of permissions themselves and of membership conditions. This structure is illustrated by Figure 2.

Figure 2. Three levels of a security policy: Enterprise, Machine, User

Security policy levels make it possible for the administrator to apply different policies of evidence evaluation and permission granting at the level of whole enterprises, individual computers and logged-in users. The process of determining the so-called effective (final) permission for the loaded assembly is therefore slightly more complicated than we described it in the section discussing code groups. Namely - having loaded the assembly the system determines its permissions by processing code groups at the highest (enterprise) policy level, after which the process is repeated at the Machine Policy level and finally at the User Policy level. The final, not always applicable level is the Application Domain, defined by the host which is loading the assembly (to discuss application domains would be beyond the scope of this article - therefore, we'd like to refer the readers to documentation of .NET Framework SDK). The CLR sums permissions granted at each level - staying faithful to the rule that permissions from a higher level can never be elevated at a lower level but can be reduced. This means that if administrators e.g. deny certain applications access to the system registry at the level of the whole enterprise, no assembly will be granted access to that registry even if the permission has been granted at the machine level.

Code Access Security (CAS)

CAS is a mechanism which controls and enforces access permissions to resources and operations which have been granted to executed code. When executed code attempts to perform an operation on a protected resource, CAS checks whether it possesses necessary permissions, then traverses the call stack upwards checking, whether the code of all previous calls possessed those permissions as well. Such a mechanism, even though it somewhat decreases performance, makes it possible to prevent scenarios where code which doesn't possess certain permissions (e.g. an application downloaded from the Internet) executes code which possesses wider permissions (e.g. from a trusted DLL present on the local machine) in order to gain unauthorised access to protected resources.

Figure 3. Examining the call stack in order to verify permissions possessed by the code

Figure 3. this very scenario. Let us look into what happens in subsequent steps:

  • code of an application downloaded from the Internet calls another part of its code (1)

  • code of an application downloaded from the Internet calls code from a trusted library, possessing the permission U1 (2)

  • code of a trusted library, possessing the permission U1, calls another fragment of its code (3)

  • code of a trusted library attempts to access a protected file, at this point CAS starts verifying whether the code possesses the permission U1 and, as the next step, whether the code of all previous calls has the permission U1 (4, 5)

  • during call verification, CAS reaches code of the application downloaded from the Internet (5) which does not possess the permission U1

  • as code was found which is not authorised to perform operations on the protected file, CAS throws an exception (an object of the type SecurityException), interrupting execution of code in the trusted library before it has gained access to the protected file (6)

  • the attempt to access a protected file from unauthorised code has failed (7)

Implementation Syntax

A programmer implementing CAS-based security mechanisms has at his disposal two methods of expressing them: imperative and declarative one. Certain operations can be performed using either of the methods, while others can be performed only declaratively.

Every standard built-in permission in .NET Framework has been defined as a class inheriting from the class CodeAccessSecurity in case of imperative syntax, as well as as a class inheriting from CodeAccessSecurityAttribute in case of declarative syntax. To illustrate this, the file access permission is represented by classes: FileIOPermission and FileIOPermissionAttribute, present in the name space System.Security.Permissions.

Declarative syntax is expressed through attributes (classes inheriting from System.Attribute), which extend metadata present in code with information about permissions. Attributes representing permissions inherit from, as mentioned above, the class CodeAccessSecurityAttribute and can refer to an assembly, a structure, a class, a constructor or a method of a class. Each of these attributes possesses the property SecurityAction which allows us to specify the kind of action we want to perform.

Here is an example of using declarative syntax in order to verify file access permissions:

 1: public class MyClass
2:
3: {
4:
5: [FileIOPermissionAttribute(SecurityAction.Demand, Unrestricted = true)]
6:
7: public void MyMethod()
8:
9: {
10:
11: }
12:
13: }

In the example we have defined a class called MyClass which possesses a method MyMethod, calls to which succeed only if the code possesses full file access permissions.

Imperative syntax involves creation of instances of classes which represent permissions and calling methods which manipulate permissions associated with code. Unlike declarative syntax, imperative syntax allows one to perform a certain operation simultaneously on a group of different permissions. This is possible thanks to classes PermissionSet and NamedPermissionSet, which represent groups of permissions. Using imperative syntax it is impossible, unlike when using declarative syntax, perform the operation of Requesting Permissions, which we will describe later on.

Here is an example of using imperative syntax:

 1: public class MyClass
2:
3: {
4:
5: public void MyMethod()
6:
7: {
8:
9: {…}
10:
11: FileIOPermission perm = new FileIOPermission (PermissionState.Unrestricted);
12:
13: perm.Demand();
14:
15: {…}
16:
17: }
18:
19: }

In the example we have defined MyClass as a class whose method MyMethod performs some operations, then verifies whether the code possesses full file access permissions and if the condition is met, executes further parts of the method.

Operations Performed on Permissions

Permission classes make available three groups of operations on permissions:

  • Requesting Permissions

  • Overriding Permissions

  • Security Demands

Security demands let the programmer define, using declarative syntax, required levels of permissions on a per-assembly basis. Permission requests are stored in the manifest of an assembly as metadata. Having loaded the assembly, created its evidence and, basing on that evidence, granted the assembly appropriate permissions, the CLR verifies whether granted permissions are not lower than the requests stored in the manifest. If the granted permissions are greater than or equal to the requested ones the code gets executed, otherwise it is not executed and a SecurityException is thrown. Such an approach makes it possible to resolve problems related with granted permissions as soon as at the stage of loading the assembly. Imagine a scenario in which our application allows its users to write documents to disc - this means it must possess FileIOPermisssion. If this permission hasn't been granted, the moment an attempt is made by the user to save his document to disc, a FileIOPermission will be thrown as a result of insufficient permissions and our application will be forced to inform the user that his document cannot be saved due to too low permissions. Such behaviour in an application can be tiring and annoying for an user - it is therefore better to specify the minimal set of necessary permissions up front, using permission requests. If the requested permissions aren't granted, the user will receive a message about insufficient permissions at application loading time, thus avoiding frustrating situations while attempting to save a freshly-created document. Defined permission requests of an assembly are also highly valuable for administrators who, using the Permission View Tool (permview.exe), can check minimal permissions for the given assembly and potentially take them into account while preparing a security policy. Furthermore, there is another advantage in requesting permissions - it enables the programmer to drop all granted permissions other than the ones which are necessary for the code to run properly; this approach greatly reduces the risk of exploiting potential flaws in our code in order to gain access to protected resources.

We distinguish three types of permissions requests; those three types can be placed simultaneously in a single assembly:

  • Request Minimum - such requests define the bare minimum of permissions, without which our code cannot run

  • Request Optional - such requests define permissions which can be granted optionally; all the others will be dropped

  • Request Refuse - such requests specify which permissions will be dropped even of the CLR has granted them

Permission requests are declared through assembly-level permission attributes, with the property SecurityAction set to RequestMinimum, RequestOptional or RequestRefuse. Several examples can be found below.

An example of using a Request Minimum which states that the minimal, necessary permissions are full access to the system registry and the ability to read/write files in the folder "c:\temp":

1: [assembly: RegistryPermissionAttribute(SecurityAction.RequestRefuse, Unrestricted=true)]
2:
3: [assembly: FileIOPermissionAttribute(SecurityAction.RequestMinimum, Write="C:\\Temp")]

An example of using a Request Optional which states that the only permission that can (but doesn't have to) be granted to the applications is access to the GUI; all other permissions will be dropped!:

1: [assembly: UIPermissionAttribute(SecurityAction.RequestOptional, Unrestricted=true)]

An example of using Request Refuse which states that the file access permission will be dropped even if it has been granted by the CLR:

1: [assembly: FileIOPermissionAttribute(SecurityAction.RequestRefuse, Unrestricted=true)]

Within requests, the programmer can also refer to built-in permission sets. An example of requesting the built-in permission level "FullTrust":

1: [assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]

It is also possible to make references to permission sets defined in XML files:

1: [assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, File = "perms.xml")]

Overriding permissions - in the beginning of the section devoted to CAS we have described the mechanism of traversing the call stack used by the CLR in order to verify that the code possesses appropriate permissions at the moment of the attempt to access a protected resource. Operations of overriding permissions allow the programmer to "trick" the CLR in an arbitrary place, influencing the mechanism of traversing the call stack and verifying code permissions by setting one of the three special override tags in the stack frame: Assert, Deny or PermitOnly. We cannot override permissions in constructors of classes in which the state of the stack can be ambiguous.

Permission-overriding tags work as follows:

Assert - presence of the Assert tag for a certain permission - assuming the code using it possesses that permission - enables access to the protected resource even if earlier calls on the stack did not have this permission. Let us return to Figure 2, in which access to the resource in "Call 4" was denied because the CLR, traversing the stack towards its top, reached "Call 2" which lacked the necessary permission U1. Setting the Assert tag for U1 in "Method 3" would cause the CLR to stop traversing the stack at that point - as a result, access to the protected file would be granted even though earlier call on the stack ("Method 2", "Method 1") didn't have the permission U1.

Here are a declarative and an imperative example of using the Assert tag in order to grant full access to a hypothetical log file:

 1: [FileIOPermissionAttribute(SecurityAction.Deny, "c:\\logs.txt")]
2:
3: public void logMessage1()
4:
5: {
6:
7: }
8:
9: public void logMessage2()
10:
11: {
12:
13: new FileIOPermission(FileIOPermissionAccess.AllAccess, "c:\\logs.txt").Deny();
14:
15: }


Deny - presence of the Deny tag for a certain permission implies that from the point where the tag has been inserted, access to resources protected by this permission will not be granted even if the code does have appropriate permissions.

Here are a declarative and an imperative example of using the Deny tag in order to revoke access to a hypothetical log file:

 1: [FileIOPermissionAttribute(SecurityAction.Deny, "c:\\logs.txt")]
2:
3: public void logMessage1()
4:
5: {
6:
7: }
8:
9: public void logMessage2()
10:
11: {
12:
13: new FileIOPermission(FileIOPermissionAccess.AllAccess, "c:\\logs.txt").Deny();
14:
15: }

PermitOnly - an opposite to Deny - implies that from the call the tag has been inserted for, access will be granted only to the resource protected by the given permission.

The programmer can clear previously-set overrides by calling one of the four static methods of the class CodeAccessPermission:

CodeAccessPermission.RevertAll - clears all defined overrides

CodeAccessPermission.RevertAssert - clears all overrides of the type Assert

CodeAccessPermission.RevertDeny - clears all overrides of the type Deny

CodeAccessPermission.RevertPermitOnly - clears all overrides of the type PermitOnly

Security demands - let the programmer declaratively (on a per-assembly, per-class, per-method etc. basis) or imperatively protect certain fragments of code by requirements of possession of certain permissions. If the code does possess these permissions, no action is taken; if it doesn't, a SecurityException is thrown.

Security demands can be implemented with the aid of three methods:

- Demand - checks with a full scan of the stack

- LinkDemand - checks with verification of the last call only

- InheritanceDemand - checks of permissions of subclasses

An imperative example of protecting a class method:

1: public static void Read()
2:
3: {
4:
5: new FileIOPermission(FileIOPermissionAccess.AllAccess, "d:\\").Demand();
6:
7: }

 

If at the moment of the call to the method Demand the code doesn't possess full permissions to perform operations on files in the path "d:\", a SecurityException will be thrown. Similarly, a method can be protected in a declarative way:

1: [FileIOPermissionAttribute(SecurityAction.Demand, AllAccess="d:\\ ")]
2:
3: public static void Read()
4:
5: {
6:
7: }

With LinkDemand, the only difference is that the CLR will not traverse the whole stack in order to verify the permission in question, checking the last call only instead.

The method InheritanceDemand allows one to limit inheritance / shadowing of of code, for classes or individual methods, only to classes possessing certain permissions.

Conclusion

The security model contained in .NET Framework is a rather complex system, providing an extensive and modular API. The structure of the security systems gives programmers almost unlimited possibilities of extending and modifying its functionality. This article discusses only a small fragment of the issue, a full description of which would be a topic for at least one whole book. A definitely invaluable source of knowledge and information here is the original documentation provided by Microsoft, available on the Web at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconsecuringyourapplication.asp


GNU General Public License
Autor: Tomasz Leszczyński Nota:
Volver