Support for Security in the Framework

 
Chapter 23 - .NET Security
bySimon Robinsonet al.
Wrox Press 2002
  

For .NET security to work, we must, as programmers, trust the CLR to enforce the security policy. How does it do this? When a call is made to a method that demands specific permissions (for example, accessing a file on the local drive), the CLR will walk up the stack to ensure that every caller in the call chain has the permissions being demanded.

The word "performance" is probably ringing in your mind at this point, and clearly that is a concern, but to gain the benefits of a managed environment like .NET this is the price we pay. The alternative is that assemblies that are not fully trusted could make calls to trusted assemblies and our system is open to attack.

For reference, the parts of the .NET Framework library namespace most applicable to this chapter are:

  • System.Security.Permissions

  • System.Security.Policy

  • System.Security.Principal

Note that evidence-based code access security works in tandem with Windows logon security. If you attempt to run a .NET desktop application, it must be granted the relevant .NET code access security permissions, but you as the logged-in user must also be running under a Windows account that has the relevant permissions to execute the code. With desktop applications, this means the current user must have been granted the relevant rights to access the relevant assembly files on the drive. For Internet applications, the account under which Internet Information Server is running must have access to the assembly files.

Demanding Permissions

Let's create a Windows Forms application that contains a button that, when clicked, will perform an action that accesses the drive. Let's say, for example, that if the application does not have the relevant permission to access the local drive ( FileIOPermission ), we will mark the button as unavailable (grayed).

In the code that follows , look at the constructor for the form that creates a FileIOPermission object, calls its Demand() method, and then acts on the result:

 using System; using System.Drawing; using System.Collections; using System.ComponentModel; using System.Windows.Forms; using System.Data; using System.Security; using System.Security.Permissions; namespace SecurityApp1 {    public class Form1 : System.Windows.Forms.Form    {       private System.Windows.Forms.Button button1;       private System.ComponentModel.Container components;       public Form1()       {          InitializeComponent();          try          {   FileIOPermission fileioperm = new     FileIOPermission(FileIOPermissionAccess.AllAccess,@"c:\");     fileioperm.Demand();   }          catch          {             button1.Enabled = false;          }       }       protected override void Dispose(bool disposing)       {          if( disposing )          {             if (components != null)              {                components.Dispose();             }          }          base.Dispose( disposing );       }       #region Windows Form Designer generated code       /// <summary>       /// Required method for Designer support - do not modify       /// the contents of this method with the code editor.       /// </summary>       private void InitializeComponent()       {          this.button1 = new System.Windows.Forms.Button();          this.SuspendLayout();          //           // button1          //           this.button1.Location = new System.Drawing.Point(48, 8);          this.button1.Name = "button1";          this.button1.Size = new System.Drawing.Size(192, 23);          this.button1.TabIndex = 0;          this.button1.Text = "Button Requires FileIOPermission";          //           // Form1          //           this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);          this.ClientSize = new System.Drawing.Size(292, 37);          this.Controls.AddRange(new System.Windows.Forms.Control[]                                 {this.button1});          this.Name = "Form1";          this.Text = "Form1";          this.ResumeLayout(false);       }       #endregion       /// <summary>       /// The main entry point for the application.       /// </summary>       [STAThread]       static void Main()        {          Application.Run(new Form1());       }    } } 

You'll notice that FileIOPermission is contained within the System.Security.Permissions namespace, which is home to the full set of permissions, and also provides classes for declarative permission attributes and enumerations for the parameters used to create permissions objects (for example, when creating a FileIOPermission specifying whether we need full access, or read-only).

If we run the application from the local drive where the default security policy allows access to local storage, the application will appear like this:

click to expand

However, if we copy the executable to a network share and run it again, we're operating within the LocalIntranet permission sets, which blocks access to local storage, and the button will be grayed:

click to expand

If we implemented the functionality to make the button access the disk when we click it, we would not have to write any security code, as the relevant class in the .NET Framework will demand the file permissions, and the CLR will ensure each caller up the stack has those permissions before proceeding. If we were to run our application from the intranet, and it attempted to open a file on the local disk, we would see an exception unless the security policy had been altered to grant access to the local drive.

If you want to catch exceptions thrown by the CLR when code attempts to act contrary to its granted permissions, you can catch the exception of the type SecurityException , which provides access to a number of useful pieces of information including a human-readable stack trace ( SecurityException.StackTrace ) and a reference to the method that threw the exception ( SecurityException.TargetSite ). SecurityException even provides us with the SecurityException.PermissionType property that returns the type of Permission object that caused the security exception to occur. If you're having problems diagnosing security exceptions, this should be one of your first ports of call. Simply remove the try and catch blocks from the above example code to see the exception in the following screenshot:

click to expand

Requesting Permissions

As you saw above, demanding permissions is where you state quite clearly what you need at run time; however, you can configure an assembly so it makes a softer request for permissions right at the start of execution where it states what it needs before it begins executing.

You can request permissions in three ways:

  • Minimum Permissions - the permissions your code must have to run

  • Optional Permissions - the permissions your code can use but is able to run effectively without

  • Refused Permissions - the permissions that you want to ensure are not granted to your code

Why would you want to request permissions when your assembly starts? There are several reasons:

  • If your assembly needs certain permissions to run, it makes sense to state this at the start of execution rather than during execution to ensure the user does not experience a road block after beginning work in your program.

  • You will only be granted the permissions you request and no more. Without explicitly requesting permissions your assembly may be granted more permissions then it needs to execute. This increases the risk of your assembly being used for malicious purposes by other code.

  • If you only request a minimum set of permissions, you are increasing the likelihood that your assembly will run since you cannot predict the security policies in effect at an end user's location.

Requesting permissions is likely to be most useful if you're doing more complex deployment, and there is a higher risk that your application will be installed on a machine that does not grant the requisite permissions. It's usually preferable for the application to know right at the start of execution if it will not be granted permissions, rather than partway through execution.

To successfully request the permissions your assembly needs, you must keep track of exactly what permissions your assembly is using. In particular, you must be aware of the permission requirements of the calls your assembly is making into other class libraries, including the .NET Framework.

Let's look at three examples from an AssemblyInfo.cs file, demonstrating using attributes to request permissions. If you are following this with the code download, these examples can be found in the SecurityApp2 project. The first attribute requests that the assembly have UIPermission granted, which will allow the application access to the user interface. The request is for the minimum permissions, so if this permission is not granted the assembly will fail to start:

   using System.Security.Permissions;     [assembly:UIPermissionAttribute(SecurityAction.RequestMinimum, Unrestricted=true)]   

Next, we have a request that the assembly be refused access to the C:\ drive. This attribute's setting means the entire assembly will be blocked from accessing this drive:

   [assembly:FileIOPermissionAttribute(SecurityAction.RequestRefuse, Read="C:\")]   

Finally, here's an attribute that requests our assembly be optionally granted the permission to access unmanaged code:

   [assembly:SecurityPermissionAttribute(SecurityAction.RequestOptional,     Flags = SecurityPermissionFlag.UnmanagedCode)]   

In this scenario we would add this attribute to an application that accesses unmanaged code in at least one place. In this case, we have specified that this permission is optional, the suggestion being that the application can run without the permission to access unmanaged code. If the assembly is not granted permission to access unmanaged code, and attempts to do so, a SecurityException will be raised, which the application should expect and handle accordingly . The full list of available SecurityAction enumeration values is shown below, and some of these values are covered in more detail later.

Security Action

Description

Assert

Allows code to access resources not available to the caller

Demand

Requires all callers in the call stack to have the specified permission

Deny

Denies a permission by forcing any subsequent demand for the permission to fail

InheritanceDemand

Requires derived classes to have the specified permission granted

LinkDemand

Requires the immediate caller to have the specified permission

PermitOnly

Similar to deny, subsequent demands for resources not explicitly listed by PermitOnly are refused.

RequestMinimum

Applied at assembly scope, this contains a permission required for an assembly to operate correctly

RequestOptional

Applied at assembly scope, this asks for permissions the assembly can use, if available, to provide additional features and functionality

RequestRefuse

Applied at assembly scope when there is a permission you do not want your assembly to have

When we are considering the permission requirements of our application, we usually have to decide between one of two options:

  • Request all the permissions we need at the start of execution, and degrade gracefully or exit if those permissions are not granted

  • Avoid requesting permissions at the start of execution, but be prepared to handle security exceptions throughout our application

Once an assembly has been configured using permission attributes in this way, we can use the permview .exe utility to view the permissions by aiming it at the assembly file containing the assembly manifest:

 >  permview.exe  <  path  >  \SecurityApp2\bin\Debug\SecurityApp2.exe  

The output for an application using the three attributes we have been through looks like this:

 Microsoft (R) .NET Framework Permission Request Viewer.  Version 1.0.3705.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. minimal permission set: <PermissionSet class="System.Security.PermissionSet"                       version="1">    <IPermission class="System.Security.Permissions.UIPermission, mscorlib,                        Version=1.0.3300.0, Culture=neutral,                         PublicKeyToken=b77a5c561934e089"                        version="1"                     Unrestricted="true"/> </PermissionSet> optional permission set: <PermissionSet class="System.Security.PermissionSet" version="1">    <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib,                        Version=1.0.3300.0, Culture=neutral,                         PublicKeyToken=b77a5c561934e089"                        version="1"                     Flags="UnmanagedCode"/> </PermissionSet> refused permission set: <PermissionSet class="System.Security.PermissionSet"                       version="1">    <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib,                        Version=1.0.3300.0, Culture=neutral,                         PublicKeyToken=b77a5c561934e089"                        version="1"                 Read="c:\"/> </PermissionSet> 

In addition to requesting permissions, we can also request permissions sets, the advantage being that we can request a whole set of permissions all at once. As the Everything permission set can be altered through the security policy while an assembly is running, it cannot be requested. For example, if an assembly requested at run time that it must be granted all permissions in the Everything permission set to execute, and the administrator then tightened the Everything permission set while the application is running, they might be unaware that it is still operating with a wider set of permissions than the policy dictates.

Here's an example, of how to request a built-in permission set:

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

In this example the assembly requests that as a minimum it be granted the FullTrust built-in permission set. If it is not granted this set of permissions, the assembly will throw a security exception at run time.

Implicit Permission

When permissions are granted, there is often an implicit statement that we are also granted other permissions. For example, if we are assigned the FileIOPermission for C:\ there is an implicit assumption that we also have access to its subdirectories (Windows account security allowing).

If you want to check whether a granted permission implicitly brings us another permission as a subset, you can do this:

   // Example from SecurityApp3     class Class1     {     static void Main(string[] args)     {     CodeAccessPermission permissionA =     new FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\");     CodeAccessPermission permissionB =     new FileIOPermission(FileIOPermissionAccess.Read, @"C:\temp");     if (permissionB.IsSubsetOf(permissionA))     {     Console.WriteLine("PermissionB is a subset of PermissionA");     }     else     {     Console.WriteLine("PermissionB is NOT a subset of PermissionA");     }     }     }   

The output looks like this:

 PermissionB is a subset of PermissionA 

Denying Permissions

There will be circumstances under which we want to perform an action and be absolutely sure that the method we call is acting within a protected environment where it cannot do anything untoward. For example, let's say we want to make a call to a third-party class in a way that we are confident it will not access the local disk.

To do that, we create an instance of the permission we want to ensure the method is not granted, and then call its Deny() method before making the call to the class:

   using System;     using System.IO;     using System.Security;     using System.Security.Permissions;     namespace SecurityApp4     {     class Class1     {     static void Main(string[] args)     {     CodeAccessPermission permission =     new FileIOPermission(FileIOPermissionAccess.AllAccess,@"C:\");     permission.Deny();     UntrustworthyClass.Method();     CodeAccessPermission.RevertDeny();     }     }     class UntrustworthyClass     {     public static void Method()     {     try     {     StreamReader din = File.OpenText(@"C:\textfile.txt");     }     catch     {     Console.WriteLine("Failed to open file");     }     }     }     }   

If you build this code the output will state Failed to open file , as the untrustworthy class does not have access to the local disk.

Note that the Deny() call is made on an instance of the permission object, whereas the RevertDeny() call is made statically. The reason for this is that the RevertDeny() call reverts all deny requests within the current stack frame; this means if you have made several calls to Deny() you only need make one follow-up call to RevertDeny() .

Asserting Permissions

Imagine that we have an assembly that has been installed with full trust on a user's system. Within that assembly is a method that saves auditing information to a text file on the local disk. If we later install an application that wants to make use of the auditing feature, it will be necessary for the application to have the relevant FileIOPermission permissions to save the data to disk.

This seems excessive, however, as really all we want to do is perform a highly restricted action on the local disk. At times like these, it would be useful if assemblies with limiting permissions could make calls to more trusted assemblies, which can temporarily increase the scope of the permissions on the stack, and perform operations on behalf of the caller that it does not have the permissions to do itself.

To achieve this, assemblies with high enough levels of trust can assert permissions that they require. If the assembly has the permissions it needs to assert additional permissions, it removes the need for callers up the stack to have such wide- ranging permissions.

The code opposite contains a class called AuditClass that implements a method called Save() , which takes a string and saves audit data to C:\audit.txt . The AuditClass method asserts the permissions it needs to add the audit lines to the file. To test it out, the Main() method for the application explicitly denies the file permission that the Audit method needs:

   using System;     using System.IO;     using System.Security;     using System.Security.Permissions;     namespace SecurityApp5     {     class Class1     {     static void Main(string[] args)     {     CodeAccessPermission permission =     new FileIOPermission(FileIOPermissionAccess.Append,     @"C:\audit.txt");     permission.Deny();     AuditClass.Save("some data to audit");     CodeAccessPermission.RevertDeny();     }     }     class AuditClass     {     public static void Save(string value)     {     try     {     FileIOPermission permission =     new FileIOPermission(FileIOPermissionAccess.Append,     @"C:\audit.txt");     permission.Assert();     FileStream stream = new FileStream(@"C:\audit.txt",     FileMode.Append, FileAccess.Write);     // code to write to audit file here...     CodeAccessPermission.RevertAssert();     Console.WriteLine("Data written to audit file");     }     catch     {     Console.WriteLine("Failed to write data to audit file");     }     }     }     }   

When this code is executed, you'll find the call to the AuditClass method does not cause a security exception, even though when it was called it did not have the required permissions to carry out the disk access.

As with RevertDeny() , RevertAssert() is a static method, and it reverts all assertions within the current frame.

It's important to be very careful when using assertions. We are explicitly assigning permissions to a method that has been called by code that may well not have those permissions, and this could open a security hole. For example, in the auditing example, even if the security policy dictated that an installed application can not write to the local disk, our assembly would be able to write to the disk when the auditing assembly asserts FileIOPermissions for writing. To perform the assertion the auditing assembly must have been installed with permission for FileIOAccess and SecurityPermission. The SecurityPermission allows an assembly to perform an assert, and the assembly will need both the SecurityPermission and the permission being asserted to complete successfully.

Creating Code Access Permissions

The .NET Framework implements code access security permissions that provide protection for the resources that it exposes. There may be occasions when you want to create your own permissions, however, and in that event you can do so by subclassing CodeAccessPermission . Deriving from this class gives you the benefits of the .NET code access security system, including stack walking and policy management.

Here are two examples of cases where you might want to roll your own code access permissions:

  • Protecting a resource not already protected by the Framework . For example, you have developed a .NET application for home automation that is implemented using an onboard hardware device. By creating your own code access permissions, you have a highly granular level of control over the access given to the home automation hardware.

  • Providing a finer degree of management than existing permissions . For example, although the .NET Framework provides permissions that allow granular control over access to the local file system, you may have an application where you want to control access to a specific file or folder much more tightly. In this scenario, you may find it useful to create a code access permission that relates specifically to that file or folder, and without that permission no managed code can access that area of the disk.

Declarative Security

You can deny, demand, and assert permissions by calling classes in the .NET Framework, but you can also use attributes and specify permission requirements declaratively .

The main benefit of using declarative security is that the settings are accessible via reflection. (It's also easier on the fingers as there's less to type!) Being able to access this information through reflection can be of enormous benefit to system administrators, who will often want to view the security requirements of applications.

For example, we can specify that a method must have permission to read from C:\ to execute:

   using System;     using System.Security.Permissions;     namespace SecurityApp6     {     class Class1     {     static void Main(string[] args)     {     MyClass.Method();     }     }     [FileIOPermission(SecurityAction.Assert, Read="C:\")]     class MyClass     {     public static void Method()     {     // implementation goes here     }     }     }   

Be aware that if you use attributes to assert or demand permissions, you cannot catch any exceptions that are raised if the action fails, as there is no imperative code around in which you can place a try-catch-finally clause.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net