2. Stack-Walk Modifiers
An object can modify the behavior of the stack walk as it passes through it. Each permission class implements the IStackWalk interface, defined as:
public interface IStackWalk
{
void Assert( );
void Demand( );
void Deny( );
void PermitOnly( );
}
The Demand( ) method of IStackWalk triggers a stack walk, and the permission classes channel the implementation to that of IPermission.Demand( ). The other three methods, Assert( ), Deny( ), and PermitOnly( ), each install a stack-walk modifier—
an instruction modifying the behavior of the walk. At any given stack
frame (i.e., the scope of a method), there can be only a single
stack-walk modifier. Trying to install a second modifier results in an
exception of type SecurityException. The stack modifier removes itself automatically once the method returns or when a call to a static reversion method of CodeAccessPermission is called, such as RevertDeny( ), RevertAssert( ), RevertPermitOnly( ), or RevertAll( ). Stack-walk modifiers
are very useful for optimizing and tightening the current security
policy, serving as a programmatic override to the configuration set by
the administrator.
2.1. Denying and permitting permissions
In general, a
component vendor can't assume that its components will always be
deployed in a properly configured and secure environment. Sometimes you
should be able to override the administrative security policy and
programmatically enforce stricter policies—for example, when the global
security policy is turned off, too liberal for your sensitive needs, or
simply unknown. Other cases may involve dealing with components from
questionable origin. Calling IStackWalk.Deny( )
denies the permission represented by the underlying permission class
and aborts the stack walk, even if the global security policy
configuration grants the calling assembly that permission. Example 3 demonstrates denying write permission to all the drives on the machine before invoking a method on a questionable component.
Example 3. Explicitly denying permissions
public void SomeMethod( ) { string[] drives = Environment.GetLogicalDrives( ); IStackWalk stackWalker; stackWalker = new FileIOPermission(FileIOPermissionAccess.Write,drives); stackWalker.Deny( ); QuestionableComponent obj = new QuestionableComponent( ); obj.DoSomething( ); CodeAccessPermission.RevertDeny( ); /* Do more work */ }
|
In this example, SomeMethod( ) constructs a new FileIOPermission object, targeting all the drives on the machine. Using the IStackWalk interface, SomeMethod( )
denies all write access to these drives. Any attempt by the
questionable component to write to these drives results in an exception
of type SecurityException. The permission denial remains in effect until the method that called Deny( )
returns. If you want to revert the denial in the scope of the calling
method (for example, to do some file I/O yourself), you need to call the
static method RevertDeny( ) of the CodeAccessPermission class, as shown in Example 3.
Sometimes it's simpler to just list what you permit, using the PermitOnly( ) method of IStackWalk. When a stack walk reaches a method that called PermitOnly( ), only that permission is presented, even if other permissions are granted administratively. Example 12-4 demonstrates permitting access only to the C:\temp directory, but nothing else.
Example 4. Permitting a particular permission only
public void SomeMethod( ) { string path = @"C:\temp"; IStackWalk stackWalker; stackWalker = new FileIOPermission(FileIOPermissionAccess.AllAccess,path); stackWalker.PermitOnly( ); QuestionableComponent obj = new QuestionableComponent( ); obj.DoSomething( ); CodeAccessPermission.RevertPermitOnly( ); /* Do more work */ }
|
Like Deny( ), PermitOnly( ) remains in effect until the calling method returns or until the method calls the static method RevertPermitOnly( ) of CodeAccessPermission.
When using a delegate
to fire an event at unknown subscribers you can explicitly deny or
permit only some permissions, which reduces the risk of calling a
delegate that may have lured you into calling it. |
|
2.2. Asserting permissions
A stack walk to verify
security permissions is a powerful and elegant idea, but it doesn't come
without a cost. A stack walk is expensive, and in intense calling
patterns or when the call stack is long, it results in a performance and
throughput penalty. Consider the following code:
void SaveString(string text,string fileName)
{
StreamWriter stream = new StreamWriter(fileName,true);//append text
using(stream)
{
stream.WriteLine(text);
}
}
string[] array = new string[9];
array[0] = "Every";
array[1] = "string";
array[2] = "in";
array[3] = "this";
array[4] = "array";
array[5] = "causes";
array[6] = "a";
array[7] = "stack";
array[8] = "walk";
string fileName = @"C:\Temp\MyStrings.txt";
foreach(string item in array)
{
SaveString(item,fileName);
}
Every time the code
writes to the file, it triggers a walk all the way up the stack. After
the first stack walk, all the subsequent stack walks are redundant,
because the call chain doesn't change between loop iterations. To
efficiently handle such cases, use the Assert( ) method of IStackWalk:
string fileName = @"C:\Temp\MyStrings.txt";
IStackWalk stackWalker;
stackWalker = new FileIOPermission(FileIOPermissionAccess.Write,fileName);
stackWalker.Assert( );
foreach(string itemin array)
{
SaveString(item,fileName);
}
CodeAccessPermission.RevertAssert( );
When asserting
security permission, stack walks demanding it stop in the current stack
frame and don't proceed up. The assertion remains in effect until the
method returns or until the static method RevertAssert( ) is called. Note that you can assert a single permission only in the same method scope (unless RevertAssert( ) or RevertAll( ) is called).
Because the stack walk
stops at the level that asserts the permission, it's quite possible that
callers further up the call chain that initiated the call don't have
the permission to do the operations carried out by the downstream
objects. It looks as if the ability to assert permissions may look like a
technique that circumvents .NET's code access security policy—that is, a
malicious assembly can assert whatever security permission it wants and
then start roaming on the machine. Fortunately, there are two
safeguards. First, if the asserting assembly isn't granted the
permission it tries to assert, the assertion has no effect. When code
down the call chain demands the asserted permission, it triggers a stack
walk. When the stack walk reaches the assertion stack-walk modifier, it
also verifies that the asserting assembly has that permission. If it
doesn't, the stack walk is aborted, and the call demanding permission
throws a security exception. The second safeguard is that not all code
can assert permissions. Only code granted the Security permission, with
the right to assert permissions, can call Assert( ).
If the permission to assert isn't granted, a security exception is
thrown on the assertion attempt. .NET doesn't use a stack walk to verify
that permission to assert is granted. Instead, it verifies that the
asserting assembly has that permission at link time.
Only the most
trustworthy code should be granted the right to assert, because of the
level of risk involved in not completing stack walks. |
|
Another important point
regarding permission assertion is that it doesn't always guarantee
stopping the stack walk, because the asserted permission may be only a
subset of the permission demand that triggered the stack walk. In that
case, the assert instruction only stops the stack walk for its
particular type and value. The stack walk may proceed up the call stack
looking for other permission grants.
2.3. Asserting unmanaged code access permission
Performance
penalties aside, the more significant side effect of the stack walk is
that it can prevent code that should run from operating. Nowhere is this
more evident than when it comes to unmanaged code access.
A potential security loophole opens when you call outside the managed-code environment using the interoperation (interop)
layer. The interop layer allows managed code to invoke calls on COM
components, or simply call DLL entry points using the platform-specific
invocation mechanism (P-Invoke).
Unmanaged code is completely exempt from .NET code access policies
because it executes directly against the operating system. Using
interop, a malicious managed component can have the unmanaged code do
its dirty work on its behalf. Naturally, only the most trustworthy
assemblies should be granted unmanaged code permission. Accordingly, the
managed side of the interop layer demands that all code accessing it
have the Security permission with the right to access unmanaged code.
The problem is, all .NET Framework classes that rely on the underlying
services of the operating system require the interop layer. Consider the
case of the FileStream class. To call it,
code requires only file I/O permission, which is a more liberal and
less powerful permission than the unmanaged code access permission.
However, because the FileStream class uses P-Invoke to call the Win32 API on behalf of the caller, any attempt to use the FileStream class triggers a demand for unmanaged code access permission by the interop layer. To shield the caller, the FileStream
class asserts the unmanaged code access permission, so it doesn't
propagate the demand for that permission to its clients. Without the
assertion, only the most trusted components could have used FileStream. Because it's signed with the Microsoft public key, the FileStream
class is granted the FullTrust permission set, and its assertion of
unmanaged code access succeeds. Instead of unmanaged code access demand,
the FileStream class demands only file I/O permission.
The next question is,
how should you handle your own interop calls to the unmanaged world? For
example, consider the following code that uses P-Invoke to import the
definition of the MessageBoxA Win32 API call, which displays a message box to the user:
using System.Runtime.InteropServices;
public class MsgBox
{
[DllImport("user32",EntryPoint="MessageBoxA")]
public static extern int Show(HandleRef handle,string text,string caption,
int msgType);
}
//Client side:
HandleRef handle = new HandleRef(null,IntPtr.Zero);
MsgBox.Show(handle,"Called using P-Invoke","Some Caption",0);
Every time the Show( )
method is called, it triggers a demand for unmanaged code access
permission. This has the detrimental effect of both a performance
penalty and a functionality impasse if the caller doesn't have generic
unmanaged code access permission but is trusted to carry out the
specific imported unmanaged call. There are a few solutions. The first
is to mimic the behavior of the .NET Framework classes and assert the
unmanaged code access permission, using a managed wrapping method around
the imported unmanaged call. Example 12-5 demonstrates this technique.
Example 5. Asserting unmanaged code access permission around an interop method
using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; public class MsgBox { [DllImport("user32",EntryPoint="MessageBoxA")] private static extern int Show(HandleRef handle,string text,string caption, int msgType); public static void Show(string text,string caption) { IStackWalk stackWalker; stackWalker = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode); stackWalker.Assert( ); HandleRef handle = new HandleRef(null,IntPtr.Zero); Show(handle,text,caption,0); } } //Client side: MsgBox.Show("Called using P-Invoke","Some Caption");
|
To assert the unmanaged code access permission, you assert the SecurityPermission permission, constructed with the SecurityPermissionFlag.UnmanagedCode
enum value. The recommended practice, however, is never to assert one
permission without demanding another permission in its place, as shown
in Example 6.
Example 6. Asserting unmanaged code permission and demanding UI permission
public static void Show(string text,string caption) { IStackWalk stackWalker; stackWalker = new SecurityPermission(SecurityPermissionFlag.UnmanagedCode); stackWalker.Assert( ); IPermission permission; permission = new UIPermission(UIPermissionWindow.SafeSubWindows); permission.Demand( ); HandleRef handle = new HandleRef(null,IntPtr.Zero); Show(handle,text,caption,0); }
|
Note that the code in Example 6 installs what amounts to a stack-walk filter.
As the stack walk makes its way through the stack frame, the filter
allows only the user-interface part of the unmanaged code demand to go
up the call stack. The assertion converts a generic demand to a more
specific demand. This is perfectly safe, because you know that the
client is not going to use the Show( ) method for all the things unmanaged code enables—the caller will use it only for user-interface purposes.
The second solution is
to suppress the interop layer's demand for unmanaged code access
permission altogether. .NET provides a special attribute called SuppressUnmanagedCodeSecurityAttribute, defined in the System.Security namespace:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method|
AttributeTargets.Interface|AttributeTargets.Delegate,
AllowMultiple = true,Inherited = false)]
public sealed class SuppressUnmanagedCodeSecurityAttribute : Attribute
{
public SuppressUnmanagedCodeSecurityAttribute( );
}
You can apply the SuppressUnmanagedCodeSecurity
attribute only to an interop method, to a class that contains interop
methods, to a delegate used to invoke an interop method, or to an
interface that the class implements. It is ignored in all other cases.
The following example shows how to apply the attribute to an interop
method:
[SuppressUnmanagedCodeSecurity]
[DllImport("user32",EntryPoint="MessageBoxA")]
public static extern int Show(HandleRef handle,string text,string caption,
int msgType);
The interop layer now will not demand that unmanaged code access permission be granted when the Show( )
method is invoked. The only safeguard is that at runtime, during the
link phase to the interop method, .NET will demand unmanaged code access
permission from the immediate caller up the stack (you will see how to
demand permission at link time later). This allows callers up the call
chain without unmanaged code access permission to call other clients
with that permission and actually invoke the interop method.
A similar issue arises
when importing COM objects to .NET, and you can also suppress demands
for unmanaged code access permission by the imported COM objects. The TlbImp command-line utility provides the /unsafe switch:
tlbimp <COM TLB or DLL name> /out:<interop assembly name> /unsafe
When you use this switch, the Runtime Callable Wrapper (RCW)—the
managed code wrapped around the COM object—will only perform link-time
demands for the unmanaged code access permission, instead of doing stack
walks on every call. Needless to say, you should use SuppressUnmanagedCodeSecurityAttribute and /unsafe with extreme caution, and only when you know that the call chain to the interop method is secure.
Since you should never assert a permission without demanding a different permission instead, in the case of using the /unsafe
switch with imported COM objects, you should build a wrapper around the
RCW and have it demand the specific required permissions. |
|
The third solution is to use security permission attributes. The next section examines this option.