Code Access Permissions
Code needs permissions in order to access a resource such as a file or to perform certain operations. A 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. You can even implement your own custom permissions for very specialized security situations. However, that is beyond the scope of this book. Here are some examples of predefined 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.
-
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.
-
UIPPermission
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 certain actions.
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.
[8]
[8]
We have not yet discussed how you set security policy, so we 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 step is
generally
superfluous because the CLR will do the demand inside of the constructor, but often you want to check permissions before you execute some code to ascertain if 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 10 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 that this code is running in has this permission, but that all the assemblies that this code is running on
behalf
of has this permission. If an exception was generated, we do not have the permission we demanded, so we then exit the program.
Dim filename As String = "..\read.txt"
' need full path for security check
Dim fileWithFullPath As String = _
Path.GetFullPath(filename)
Try
Dim fileIOPerm As FileIOPermission = _
New FileIOPermission(_
FileIOPermissionAccess.AllAccess, _
fileWithFullPath)
fileIOPerm.Demand()
Catch e As Exception
Console.WriteLine(e.Message)
Return
End Try
Try
Dim file As FileInfo = New FileInfo(filename)
Dim sr As StreamReader =
File.OpenText()
Dim text As String
text =
sr.ReadLine()
While Not text Is Nothing
Console.WriteLine(text)
text =
sr.ReadLine()
End While
sr.Close()
Catch e As Exception
Console.WriteLine(e.Message)
End Try
Even if the code has the CLR read permission, the user must have read permission from the file system. If the user does not, the
UnauthorizedAccessException
will be thrown when the
OpenText
method is called.
You have to be careful when 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 further check is made by the CLR to ascertain access rights. The assembly you pass the object to might 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 will 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
performs
checks on all the callers on the stack frame to make sure that each assembly that has a method call 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 is not permitted to use trusted code to perform an unauthorized action. 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 that it cannot open your email address book, delete files, and so on.
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 roll back several operations. In some situations, the user could be told up front that certain functions will not be available, so that he or she will not be surprised later when certain operations are blocked.
On the other hand, 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, there is no good reason to permit it, and therefore, you should explicitly deny 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. We then try to read the file in a separate method named
ReadFile
.
...
Try
fileIOPerm.
Deny
()
Console.WriteLine("File Access Permission Denied")
Catch se As SecurityException
Console.WriteLine(se.Message)
End Try
ReadFile()
...
The reason we attempt to read the file in the separate
ReadFile
method will be explained shortly, when we discuss the
Assert
method. For now, notice that since the permission was denied, the
FileInfo
constructor in the
ReadFile
method will throw a
SecurityException
.
Public Sub
ReadFile
()
Try
Dim file As FileInfo =
New FileInfo(filename)
Dim sr As StreamReader = File.OpenText()
Dim text As String
Text = sr.ReadLine()
While text <> Nothing
Console.WriteLine(" " + text)
text = sr.ReadLine()
End While
sr.Close()
Catch se As SecurityException
Console.WriteLine(_
"Could not read file: " + se.Message)
End Try
End Sub
We then call the static
RevertDeny
method on the
FileIOPermission
class to remove the permission denial, and attempt to read the file again. This time the file can be read without throwing an exception. 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.
...
Try
FileIOPermission.RevertDeny()
Console.WriteLine(_
"File Access Permission Restored")
Catch se As SecurityException
Console.WriteLine(se.Message)
End Try
ReadFile()
...
We then invoke the
Deny
method to once again remove the permission and attempt to read the file, throwing an exception.
...
Try
fileIOPerm.
Deny()
Console.WriteLine(_
"File Access Permission Denied")
Catch se As SecurityException
Console.WriteLine(se.Message)
End Try
ReadFile()
...
Asserting
Permissions
The
Assert
method allows you to demand a permission even though you do not have access rights to do so due to callers higher in the stack that have not been granted the necessary permission. Of course, you can only assert permissions that your assembly itself has been granted. If this were not the case, it would be trivial to circumvent CLR security.
The
SimplePermissionCodeDenial
example now asserts the
FileIO-Permission
and then attempts to read the file by calling the
ReadFile
method, then the
ReadFileWithAssert
method, and finally the
ReadFile
method again. The
ReadFileWithAssert
method is much the same as the
ReadFile
method, but it has its own call to the
Assert
method.
Try
fileIOPerm.Assert()
Console.WriteLine(_
"File Access Permission Asserted")
Catch se As SecurityException
Console.WriteLine(se.Message)
End Try
ReadFile()
ReadFileWithAssert(fileIOPerm)
Console.WriteLine(_
"Returned from read routine with assert.")
ReadFile()
The file-read operations in both calls to the
ReadFile
method fail, but the file-read in
ReadFileWithAssert
succeeds. The permission assertion is only good within the method that calls
Assert
, and the assertion disappears after returning from that method. 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. Similarly,
Deny
prevents
callers higher in the stack frame from an action, but not on the current level.
Public Sub
ReadFileWithAssert
(ByVal f As FileIOPermission)
Try
f.
Assert()
Console.WriteLine(_
"File Permission Asserted in same procedure
as read.")
Dim file As FileInfo = New FileInfo(filename)
Dim sr As StreamReader = file.OpenText()
Dim text As String
text = sr.
ReadLine()
While text <> Nothing
Console.WriteLine(" " + text)
text = sr.
ReadLine()
End While
sr.Close()
Catch se As SecurityException
Console.WriteLine(_
"Could not read file: " + se.Message)
End Try
End Sub
Remember that the permission assertion only applies to I/O operations done in this one method for the specific file that was passed into the
FileIO-Permission
constructor (i.e., read.txt). The call to
Assert
is good until the containing method 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.
Other Permission Methods
PermitOnly
specifies the permissions that should succeed. You simply 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 "meta permissions" 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.
Dim ap As AppDomain = 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 Nothing 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.
...
Dim sp As SecurityPermission = _
New SecurityPermission(_
SecurityPermissionFlag.
ControlPrincipal
)
Try
sp.
Demand()
Catch se As SecurityException
Console.WriteLine(se.Message)
Return
End Try
...
We first construct a new
SecurityPermission
instance, passing to the constructor the security permission we want to see if 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 only be granted to trusted code. We then demand (i.e., 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
Security-
Permission
class to prevent components from calling
Assert
. Construct an instance of the class with the
SecurityPermissionFlag.Assertion
value, and then
Deny
the permission. Among the other actions you can control with the
SecurityPermission
class are the ability to create and manipulate application domains, specify policy, allow or disallow execution, control whether verification is performed, and access unmanaged code.
Calling Unmanaged Code
Asserts are necessary for controlling access to unmanaged code, because in order to call unmanaged code, you need the unmanaged code permission, since the CLR performs a stack walk to check if all the callers have the unmanaged code permission. Therefore, if you did not use the
Assert
method, you would have to grant all code the unmanaged code permission. Hence, assemblies other than your own trusted assemblies could perform operations through the Win32 API calls and subvert the framework's security system.
[9]
[9]
The underlying operating system identity that is running the program must have the rights to perform the operating system function.
It is much better to make unmanaged code calls through wrapper classes that are contained in an assembly that has the unmanaged code permission. 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 call unmanaged code.
[10]
No other assembly in the call chain then needs to have the unmanaged code permission.
[10]
By demanding first and then asserting, you can ensure that a
luring
attack (i.e.,
unprivileged
code calling privileged code to do evil things) 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 unmanaged 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: SecurityPermissionAttribute(_
SecurityAction.RequestMinimum, _
ControlPrincipal:=True
)>
...
The
SecurityAction
enumeration has several values, some of which can be applied to a class or method and some that can be applied to an entire 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.
[11]
[11]
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. The named value
All
means all file access is being demanded for the specified file. A full file path is required.
<FileIOPermissionAttribute(SecurityAction.Demand, _
All:="c:\foo\read.txt")> _
Public Class Simple
...
End Class
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.
Dim PrincipalPerm As PrincipalPermission = _
New PrincipalPermission(_
wi.Name, adminRole)
Try
PrincipalPerm.
Demand
()
Console.WriteLine(_
"Code demand for an administrator succeeded.")
Catch e As SecurityException
Console.WriteLine(_
"Demand for Administrator failed.")
End Try
If the current user were an administrator, the demand would succeed;
otherwise
, it would fail with an exception being thrown. The code then checks for the user with the name JaneAdmin (not a system administrator, but part of the CustomerAdmin
group
).
Dim customerAdminRole As String = _
"HPDESKTOP\CustomerAdmin"
Dim pp As PrincipalPermission
pp = New PrincipalPermission(_
"HPDESKTOP\JaneAdmin", customerAdminRole)
Try
pp.Demand()
Console.WriteLine(_
"Demand for Customer Administrator succeeded.")
Catch e As SecurityException
Console.WriteLine(_
"Demand for Customer Administrator failed.")
End Try
Next, the
PrincipalPermission
example tests to see if either of these two administrators is the identity of the running code. The
PrincipalPermission
class has methods for creating permissions that are the union or the intersection of other permissions. The following shows how to form a union of the permissions of the users Administrator and PeterT.
Dim
id1
As String =
"HPDESKTOP\Administrator"
Dim
id2
As String =
"HPDESKTOP\PeterT"
Dim
pp1
As PrincipalPermission = _
New
PrincipalPermission
(_
id1
, adminRole)
Dim
pp2
As PrincipalPermission = _
New
PrincipalPermission
(_
id2
, adminRole)
Dim
ipermission
As IPermission =
pp2.Union(pp1)
Try
ipermission.Demand()
Console.WriteLine(_
"Demand for either administrator succeeded.")
Catch e As SecurityException
Console.WriteLine(_
"Demand for either administrator failed.")
End Try
The code then sees if any administrator is the current identity of the running code.
Dim
pp3
As PrincipalPermission = _
New
PrincipalPermission
(_
Nothing
, adminRole)
Try
pp3.
Demand()
Console.WriteLine(_
"Demand for any administrator succeeded.")
Catch e As SecurityException
Console.WriteLine(_
"Demand for any administrator failed.")
End Try
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 permissions. 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
Function
PotentialRogueCode
() As Integer
End Interface
Public Class ThirdParty
Implements IUserCode
Public Function
PotentialRogueCode
() As Integer _
Implements IUserCode.PotentialRogueCode
Try
Dim filename As String = "..\read.txt"
Dim file As FileInfo = New FileInfo(filename)
Dim sr As StreamReader = file.OpenText()
Dim text As String
text = sr.ReadLine()
While text <> Nothing
Console.WriteLine(text)
text = sr.ReadLine()
End While
sr.Close()
Catch e As Exception
Console.WriteLine(e.Message)
End Try
Return 0
End Function
End Class
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 Module PSTest
Public Sub Main()
Dim thirdParty As ThirdParty = New ThirdParty()
Dim ourClass As OurClass = New OurClass()
ourClass.
OurCode
(thirdParty)
End Sub
End Module
...
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. The third-party code is then called. After it returns, the permission denial is
revoked
and the third-party code is called again.
Public Class OurClass
Public Sub
OurCode
(ByVal code As IUserCode)
Dim uiPerm As UIPermission = _
New UIPermission(PermissionState.Unrestricted)
Dim fileIOPerm As FileIOPermission = _
New FileIOPermission(PermissionState.Unrestricted)
Dim ps As PermissionSet = _
New
PermissionSet
(_
PermissionState.None)
ps.
AddPermission(uiPerm)
ps.
AddPermission(fileIOPerm)
ps.
Deny()
Console.WriteLine("Permissions denied.")
Dim v As Integer = code.
PotentialRogueCode()
CodeAccessPermission.
RevertDeny()
Console.WriteLine("Permissions allowed.")
v = code.
PotentialRogueCode()
End Sub
End Class
The first time the
PotentialRogueCode
method is called, it fails, and the second time, it succeeds. Each stack frame can have only 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.
|