Code Access Permissions

for RuBoard

Code needs permissions in order to access a resource such as a file, or perform some operation. Security Policy (discussed later in the chapter) will give certain permissions to each assembly. Code access permissions can be requested by code. The CLR will decide which permissions to grant based on the security policy for that assembly. We will not discuss how to write a custom permission.

Here are some examples of Code access permissions:

  • DNSPermission controls access to Domain Name servers on the network.

  • EnvironmentPermission controls read or write access to environment variables .

  • FileIOPermission controls access to files and directories.

  • FileDialogPermission allows files selected in an Open dialog box to be read. This is useful if FileIOPermission has not been granted.

  • ReflectionPermission controls the ability to access nonpublic metadata and emit metadata.

  • RegistryPermission controls the ability to access and modify the registry.

  • SecurityPermission controls the use of the security subsystem.

  • SocketPermission controls the ability to make or accept connections on a transport address.

  • UIPermission controls the user of various user-interface features including the clipboard.

  • WebPermission controls making or accepting connections on a Web address.

The use of these permissions is referred to as Code Access Security because this permission is based not on the identity of the user running the code, but on whether the code itself has the right to take some action.

Simple Permission Code Request

The SimplePermissionCodeRequest example first requests permission to access a file. If the CLR does not grant that request, the CLR will throw a SecurityException inside the file constructor. However, this code first tests to see if it has that permission. If it does not, it just returns instead of trying to access the file. [20]

[20] We have not yet discussed how you set security policy so you do not yet know how to grant or revoke this permission. By default, however, code running on the same machine that it resides on has this permission. This is another example of how difficult it is to talk about security without knowing the whole picture.

This step is generally superfluous because the CLR will do the demand inside the constructor, but often you want to check permissions before you execute some code to ascertain whether you have the rights you need.

The FileIOPermission class models the CLR file permissions. A full path must be supplied to its constructor, and we use the Path class we discussed in Chapter 8 to get the full path. We are asking for read, write, and append file access. Other possible access rights are NoAccess or PathDiscovery . The latter is required to access information about the file path itself. You might want to allow access to the file, but you may want to hide information in the path such as directory structure or user names .

The demand request checks to see if we have the required permission. The Demand method checks all the callers on the stack to see if they have this permission. In other words, we want to make sure not only that the assembly this code is running in has this right, but that all the assemblies this code is running on behalf of have this permission. If an exception was generated, we do not have the right we demanded, so we exit the program.

 string filename = ".\read.txt";  string fileWithFullPath = Path.GetFullPath(filename);  try  {    FileIOPermission fileIOPerm = new          FileIOPermission(FileIOPermissionAccess.AllAccess,                                        fileWithFullPath);    fileIOPerm.Demand();  }  catch(Exception e)  {    Console.WriteLine(e.Message);    return 1;  }  try  {    FileInfo file = new FileInfo(filename);    StreamReader sr = file.OpenText();    string text;    text = sr.ReadLine();    while (text != null)    {      Console.WriteLine(text);      text = sr.ReadLine();    }    sr.Close();  }  catch(Exception e)  {    Console.WriteLine(e.Message);  } 

Even if the code has the CLR read permission, the user must have read permission from the file system. If the user does not, an UnauthorizedAccessException will be thrown when the OpenText method is called.

You have to be careful in passing objects that have passed a security check in their constructor to code in other assemblies. Since the check was made in the constructor, no other check is made by the CLR to ascertain access rights. The assembly you pass the object to may not have the same rights as your assembly. If you were to pass this FileInfo object to another assembly that did not have the CLR read permission, it would not be prevented from accessing the file by the CLR, because no additional security check would be made. This is a design compromise for performance reasons to avoid making security checks for every operation. This is true for other code access permissions as well.

How a Permission Request Works

To determine whether code is authorized to access a resource or perform an operation, the CLR checks all the callers on the stack frame, making sure that each assembly that has a method on the stack can be granted the requested permission. If any caller in the stack does not have the permission that was demanded, a SecurityException is thrown.

Less trusted code cannot use trusted code to perform an unauthorized action (" luring attack"). The procedures on the stack could come from different assemblies that have different sets of permissions. For example, an assembly that you build might have all rights, but it might be called by a downloaded component that you would want to have restricted rights (so it doesn't open your email address book).

As discussed in the next sections, you can modify the results of the stack walk by using Deny or Assert methods on the CodeAccessPermission base class.

Strategy for Requesting Permissions

Code should request permissions that it needs before it uses them, so that it is easier to recover if the permission request is denied . For example, if you need to access several key files, it is much easier to check to see if you have the permissions when the code starts up rather than when you are halfway through a delicate operation and then have to recover. Users could be told up front that certain functions will not be available to them. Or, as we will discuss later, you could use assembly permission requests, and then fail to load if the required permissions are not present. The problem is that you may not know what permissions request will succeed because you do not know what assemblies will have callers on the stack when the request is made.

You should not request permissions that you do not need. This will minimize the chances that your code will do damaging things from bugs or malicious third-party code and components . In fact you can restrict the permissions you have to the minimum necessary to prevent such damage. For example, if you do not want a program to read and write the files on your disk, you can deny it the right to do so.

Denying Permissions

One can apply the Deny method to the permission. Even though security policy would permit access to the file, any attempt to access the file will fail. The SimplePermissionCodeDenial example demonstrates this. Instead of demanding the permission, we invoke the Deny method on the FileIOPermission object.

 ...  try  {    fileIOPerm.Deny();    Console.WriteLine("File Access Permission Removed");  }  catch(SecurityException se)  {    Console.WriteLine(se.Message);  } 

We then try to read the file using the ReadFile method. Why we do this inside another method will be explained shortly. Since the permission was denied, the FileInfo constructor will throw a SecurityException .

 ...  try  {    FileInfo file = new FileInfo(filename);    StreamReader sr = file.OpenText();    string text;    text = sr.ReadLine();    while (text != null)    {      Console.WriteLine(""+ text);      text = sr.ReadLine();    }    sr.Close();  }  catch(SecurityException se)  {    Console.WriteLine("Could not read file: " +  se.Message);  } 

We then call the static RevertDeny method on the FileIOPermission class to remove the permission denial, and we attempt to read the file again. This time the file can be read. The call to Deny is good until the containing code returns to its caller or a subsequent call to Deny . RevertDeny removes all current Deny requests.

 ...  FileIOPermission.RevertDeny();  ...  ReadFile(); 

We then invoke the Deny method to once again remove the permission.

Asserting Permissions

The Assert method allows you to demand a permission even though you do not have access rights to do so. You might also want to assert a permission because other calls in the call chain do not have the right, even though your assembly does. You can only assert permissions that your assembly has been granted. If this were otherwise , it would be trivial to circumvent CLR security. [21]

[21] You also need the permission to assert.

The test program code now asserts the FileIOPermission and then attempts to read the file.

 ...  ...  fileIOPerm.Deny();  ...  fileIOPerm.Assert();  ...  ReadFile();  ReadFileWithAssert(fileIOPerm);  ...  ReadFile(); 

But the file read fails! The assertion is good only within the method that called. The ReadFileWithAssert method can read the file because it asserts the permission within the method and then attempts the read. Assert stops the permission stack walk from checking permissions higher in the stack frame and allows the action to proceed, but it does not cause a grant of the permission. Therefore, if code further down the stack frame (like ReadFile ) tries to demand the denied permission (as the FileInfo constructor does), a SecurityException will be thrown. [22] Similarly, Deny prevents callers higher in the stack frame from an action, but not on the current level.

[22] This is true as well for code above you on the stack frame.

 public static void ReadFileWithAssert(FileIOPermission f)  {    ...     f.Assert();     ...     FileInfo file = new FileInfo(filename);     StreamReader sr = file.OpenText();     string text;     text = sr.ReadLine();     while (text != null)     {       Console.WriteLine(""+ text);       text = sr.ReadLine();      }     sr.Close();   ...  } 

Remember that the assert applies only to IO operations done in this routine for the specific file that was passed the FileIOPermission constructor. The call to Assert is good until the containing code returns. Hence, ReadFile fails again when it is attempted after ReadFileWithAssert returns. RevertAssert removes all current Assert requests.

Assert opens up security holes, because some caller in the stack frame might be able to use the routine that calls assert to violate security. Any use of Assert should be subject to a security review.

Other Permission Methods

PermitOnly specifies the permissions that should succeed. You specify what resources you want to access. The call to PermitOnly is good until the containing code returns, or a subsequent call to PermitOnly . RevertPermitOnly removes all current PermitOnly requests. RevertAll removes the effect of Deny , PermitOnly , and Assert .

SecurityPermission Class

The SecurityPermission class controls "metapermissions" that govern the CLR security subsystem. Let us look again at the RoleBasedSecurity example from earlier in the chapter. It used the AppDomain.SetPrincipalPolicy method to set the application domain's principal policy:

 AppDomain ap = AppDomain.CurrentDomain;  ap.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal); 

The type of principal returned by Thread.CurrentPrincipal will depend on the Application Domain's Principal Policy. An Application Domain can have one of three authentication policies as defined by the System.Security.PrincipalPolicy enumeration:

  • WindowsPrincipal uses the current user associated with the thread. Thread.CurrentPrincipal returns a WindowsPrincipal object.

  • UnauthenticatedPrincipal uses an unauthenticated user. Thread.CurrentPrincipal returns a GenericPrincipal object. This is the default.

  • NoPrincipal returns null for Thread.CurrentPrincipal .

You set the policy with the SetPrincipalPolicy method on the AppDomain instance for the current application domain. The static method AppDomain.CurrentDomain will return the current instance. This method should be called before any call to Thread.CurrentPrincipal , because the principal object is not created until the first attempt to access that property.

In order for the RoleBasedSecurity example to set the principal policy it needs to have the ControlPrincipal right. To ascertain if the executing code has that right, you can demand that SecurityPermission before you change the policy. A SecurityException will be thrown if you do not have that permission.

 ...  SecurityPermission sp = new SecurityPermision(  SecurityPermissionFlag.ControlPrincipal);    try  {    sp.Demand();  }  catch(SecurityException se)  {    Console.WriteLine(se.Message);    return 1;  } 

We first construct a new SecurityPermission instance, passing to the constructor the security permission we want to see whether we have the right to use. SecurityPermissionFlag is an enumeration of permissions used by the SecurityPermission class. The ControlPolicy permission represents the right to change policy. Obviously, this should be granted only to trusted code. We then demand (request) the permission.

As mentioned earlier, you can only assert permissions that your assembly actually has. So rogue components cannot just assert permissions when running within your code. You can either set security policy or use the SecurityPermission class to prevent components from calling Assert . Construct an instance of the class with the SecurityPermissionFlag.Assertion value and then Deny the permission. Other actions you can control with the SecurityPermission class include the ability to create and manipulate application domains, specify policy, allow or disallow execution, control whether verification is performed, or access unmanaged code.

Unmanaged Code

Asserts are necessary for controlling access to unmanaged code, since managed code should not call unmanaged code directly.

In order to call unmanaged code you need the unmanaged code permission. [23] Since the CLR performs a stack walk to check whether all the callers have unmanaged code permission, you would have to grant all code the unmanaged code permission. Hence, assemblies other than your own trusted ones could perform operations through the Win32 API calls and subvert the framework's security system. [24]

[23] As with all the other "security permissions" this is technically a flag on the SecurityPermission class, but the common parlance is to call them permissions.

[24] The underlying operating system identity that is running the program must have the rights to perform the operating system function.

Better would be to make calls through wrapper classes that are contained in an assembly that has the managed-code right. The code in the wrapper class would first ascertain that the caller has the proper CLR rights by demanding the minimal set of permissions necessary to accomplish the task (such as writing to a file). If the demand succeeds, then the wrapper code can assert the right to managed code. [25] No other assembly in the call chain then needs to have the managed-code right.

[25] By demanding first, then asserting, you ensure that a luring attack is not in progress.

For example, if you ask the .NET file classes to delete a file, they first demand the delete permission on the file. If that permission is granted, then the code asserts the managed code permission and calls the Win32 API to perform the delete.

Attribute-Based Permissions

The SimplePermissionAttributeRequest example shows how you can use attributes to make permission requests. This example uses an attribute to put in the metadata for the assembly that you need to have the ControlPrincipal permission to run. This enables you to query in advance which components conflict with security policy.

 [assembly:SecurityPermission(       SecurityAction.RequestMinimum,ControlPrincipal=true)]  public class pp  {      public static int Main(string[] args)  ... 

The SecurityAction enumeration has several values, some that can be applied to a class or method and some that can be applied to an assembly as in this example. For assemblies these are RequestMinimum , RequestOptional , and RequestRefuse . RequestMinimum indicates to the metadata those permissions the assembly requires to run. RequestOptional indicates to the metadata permissions that the assembly would like to have, but can run without. RequestRefuse indicates permissions that the assembly would like to be denied. [26]

[26] An assembly would do this to prevent code from another assembly executing on its behalf from having this permission.

If you change the attribute in this example to RequestRefuse and run it, you will find that the assembly will load, but you will get a SecurityException when you attempt to change the policy.

Other values apply to classes and methods. LinkDemand is acted upon when a link is made to some type. It requires your immediate caller to have a permission. The other values apply at runtime. InheritanceDemand requires a derived class to have a permission. Assert , Deny , PermitOnly , and Demand do what you would expect.

Here is an example of a FileIOPermission demand being applied to a class through an attribute. AllAccess is being demanded of the file. A full file path is required.

 [FileIOPermission(SecurityAction.Demand,                               All = "c:\foo\read.txt")]  public class Simple  ... 

Principal Permission

Role-based security is controlled by the PrincipalPermission class. The PrincipalPermission example uses this class to make sure that the user identity under which the program is being run is an administrator. We do that by passing the identity name and a string representing the role to the constructor. Once again, we use the Demand method on the permission to check the validity of our permission request.

 PrincipalPermission PrincipalPerm = new                  PrincipalPermission(wi.Name, adminRole);  try  {    PrincipalPerm.Demand();    Console.WriteLine("Code demand for an administrator                                             succeeded.");  }  catch(SecurityException)  {    Console.WriteLine("Demand for Administrator failed.");  } 

If the running user were an administrator the demand would succeed; otherwise it would fail with an exception being thrown. The code then checks to see if the user with the name JaneAdmin (not a system administrator, but part of the CustomerAdmin group ) and the designated role is running.

 string customerAdminRole = "MICAH\CustomerAdmin";  PrincipalPermission pp;  pp = new PrincipalPermission("MICAH\JaneAdmin",                                       customerAdminRole);  try  {    pp.Demand();    Console.WriteLine("Demand for Customer Administrator                                             succeeded.");  }  catch(SecurityException)  {    Console.WriteLine("Demand for Customer Administrator                                                 failed.");  } 

The CodeAccessPermission base class has methods for creating permissions that are the union or the intersection of several permissions. PrincipalPermission does not derive from CodeAccessPermission because it is based on the identity associated with the code, not on the rights of the code itself. Nonetheless, it shares the same idioms with the CodeAccessPermission derived classes.

Next the example code sees if either of these two administrators is the identity of the running code.

 string id1 = "MICAH\Administrator";  string id2 = "MICAH\mds";  PrincipalPermission pp1 = new PrincipalPermission(id1,                                               adminRole);  PrincipalPermission pp2 = new PrincipalPermission(id2,                                               adminRole);  IPermission ipermission = pp2.Union(pp1);  try  {    ipermission.Demand();    Console.WriteLine("Demand for either administrator                                             succeeded.");  }  catch(SecurityException)  {     Console.WriteLine("Demand for either administrator                                                failed.");  } 

The code then sees whether any administrator is the identity of the running code. [27]

[27] A null user and a role as arguments to mean anyone in that role is not an intuitive use of null.

 PrincipalPermission pp3 = new PrincipalPermission(null,                                               adminRole);  try  {    pp3.Demand();    Console.WriteLine("Demand for any administrator                                             succeeded.");  }  catch(SecurityException)  {    Console.WriteLine("Demand for any administrator                                                failed.");  } 

If the users are unauthenticated, even if they do belong to the appropriate roles, the Demand will fail.

PermissionSet

You can deal with a set of permissions through the PermissionSet class. The AddPermission and RemovePermission methods allow you to add instances of a CodeAccessPermission derived class to the set. You can then Deny , PermitOnly , or Assert sets of permissions instead of individual ones. This makes it easier to restrict what third-party components and scripts might be able to do. The PermissionSet example demonstrates how this is done.

We first define an interface IUserCode that our "trusted" code will use to access some "third-party" code. While in reality this third-party code would be in a separate assembly, to keep the example simple we put everything in the same assembly.

 public interface IUserCode  {        int PotentialRogueCode();  }  public class ThirdParty : IUserCode  {     public int PotentialRogueCode()     {         try        {           string filename = ".\read.txt";           FileInfo file = new FileInfo(filename);           StreamReader sr = file.OpenText();           string text;           text = sr.ReadLine();           while (text != null)           {              Console.WriteLine(text);              text = sr.ReadLine();           }           sr.Close();        }        catch(Exception e)        {           Console.WriteLine(e.Message);        }        return 0;     }  } 

Our code will create a new instance of the "third party" which would cause the code to be loaded into our assembly. We then invoke the OurCode method passing it the "third-party" code.

 ...  public static int Main(string[] args)  {    ThirdParty thirdParty = new ThirdParty();    OurClass ourClass = new OurClass();    ourClass.OurCode(thirdParty);    return 0;  } 

Now let us look at the OurCode method. It creates a permission set consisting of unrestricted user interface and file access permissions. It then denies the permissions in the permission set.

 ...  public void OurCode(IUserCode code)  {  UIPermission uiPerm = new               UIPermission(PermissionState.Unrestricted);   FileIOPermission fileIOPerm = new           FileIOPermission(PermissionState.Unrestricted);  PermissionSet ps = new                      PermissionSet(PermissionState.None);  ps.AddPermission(uiPerm);  ps.AddPermission(fileIOPerm);  ps.Deny();  ... 

The "third-party" code is then called. After it returns, the permission denial is revoked and the "third-party" code is called again.

 int v = code.PotentialRogueCode();  CodeAccessPermission.RevertDeny();  ...  v = code.PotentialRogueCode(); 

The first time, the code execution fails; the second time it succeeds. Each stack frame can only have one permission set for denial of permissions. If you call Deny on a permission set, it overrides any other calls to Deny on a permission set in that stack frame.

for RuBoard


Application Development Using C# and .NET
Application Development Using C# and .NET
ISBN: 013093383X
EAN: 2147483647
Year: 2001
Pages: 158

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