Being Careful About What Code Gets Executed

for RuBoard

Beyond simply protecting data your application uses, there are some special cases where you may end up executing code that you didn't intend to execute. Those special cases include the following:

  • LinkDemand s and inheritance

  • Virtual, internal methods

  • Delegates and stack walks

  • Loading assemblies on behalf of other code

  • Exceptions and filters

  • Race conditions

LinkDemand s and Inheritance

It is of the utmost importance to remember that LinkDemand s trigger when a method is JITted, not while a method is actually executing. Thus, applying LinkDemand s to interfaces, classes, and virtual methods has a big caveat. If there is an inheritance hierarchy where LinkDemand s are not applied uniformly for a given method, malicious code can downcast an object to a class or interface that doesn't have a LinkDemand on that method. By doing so, that code can avoid a security check by executing a base class method.

If you are using LinkDemand s on any interface, unsealed class, or virtual method, you should be careful to uniformly apply those LinkDemand s across the entire hierarchy.

Virtual, Internal Methods

When a method is marked as internal, you might intuitively feel that you have complete control over the code you run when that method gets executed. However, if that method is also marked as virtual, it can still be overridden on subclasses. Thus, if you accept objects from outside your code, and if you execute virtual, internal methods on those objects, you must assume that those methods were overridden. In some cases, this might be by design. However, this could also lead to security holes if trust decisions are based on the output of the virtual, internal method.

You should avoid marking methods as virtual and internal unless you plan on leveraging both the fact that it is virtual and internal. From our experience, this has been done more often accidentally than from intentional design.

Delegates and Stack Walks

Delegates are much like function pointers in C/C++. They are used most often to enable events in the .NET Framework. However, managed code can perform security stack walks when running delegates as a result of permission demands, while unmanaged code has no such mechanism with function pointers. If your application needs to run a delegate on behalf of other code, those stack walks will happen on your application's current call stack instead of the call stack from whatever code handed the delegate to your application. Thus, haphazard use of delegates could lead to luring attacks.

There are two limiting factors that prevent code with very low trust from picking malicious methods and getting them run successfully by code with more trust. First, if the delegate was defined in an assembly with low trust, a stack walk will include that assembly with low trust on the stack. This will stop malicious code from simply defining arbitrary methods to use as delegates. Second, a method's signature must match the defined delegate signature. That means the malicious code must find a dangerous method with a precise signature before it can treat it as a delegate.

The best way to protect your application from running malicious delegates is to carefully define any delegate signatures you use. You probably want to include a class that you define in the argument list. This will prevent any signature matches from happening with methods shipped as part of the .NET Framework. It would be unfortunate, for example, if you defined a delegate with a single integer argument. If you did that, malicious code could register the method Environment.Exit for you to run. Thus, malicious code could get you to terminate your own application prematurely.

One other action you should consider before running delegates is to use a permission Deny or PermitOnly . For example, if you only want to allow the delegates you call to display UI, you could use the following statement:

 New UIPermission(PermissionState.Unrestricted).PermitOnly(); 

This would prevent any stack walks from succeeding if a demand occurred for a permission other than the UIPermission .

Loading Assemblies

Loading assemblies on behalf of other code is something you may not realize could be dangerous. As long as you just load an assembly from the global assembly cache or a specific path , you should be fine. However, there are some assembly loading methods you should be careful using:

  • Any form of Assembly.Load or AppDomain.Load that loads an assembly from a byte array

  • Any form of AppDomain.DefineDynamicAssembly

  • Any methods that load an assembly and have an Evidence argument

When an assembly is loaded from a byte array, the .NET Framework cannot tell some types of evidence about that assembly, such as from what zone the assembly originated. Because of this, the .NET Framework grants the same permissions to the byte array assembly as were granted to the assembly loading the byte array assembly. For example, if you write assembly A that loads assembly B from a byte array, assembly B is granted the same permissions as assembly A. Therefore, you don't want to load arbitrary assemblies from byte arrays on behalf of unknown code. You might end up providing an easy way for malicious code to gain permissions.

The results are very similar for defining a dynamic assembly on behalf of another assembly. If your code immediately runs the dynamic assembly, it will have the same permissions granted to it as your code was granted. Thus, you need to be as careful with dynamic assemblies as you are with loading assemblies from byte arrays.

The third category to watch is any method that loads or defines an assembly while providing evidence about that assembly. You should be careful not to provide evidence that grants the code an inappropriate amount of trust. For example, if you provide zone evidence that says the assembly comes from the MyComputer zone, the assembly you load will be given full trust when used with default security policy.

Exceptions and Filters

User -filtered exception handlers are a feature of the .NET Framework that may be unexpected if you have only used C#. They are not used by C#, but VB.NET does take advantage of this feature using the When keyword. Their purpose is to determine whether to handle an exception at a given level or to pass it up to the next protection block.

When using a try / finally scheme for exception handling, you need to realize that user-filtered exception handlers may run between your try block and finally block. This can cause problems if you change sensitive global state in the try block that gets reverted in the finally block. For example, if you impersonated a different user in a try block and planned on reverting back to the original user in the finally block, semi-trusted code you didn't write could execute as the impersonated user. Listing 29.3 shows an example of the execution order when user-filtered exception handlers are implemented. The example is written in VB.NET because C# does not have user-filtered exception handlers. To get a feeling for how these filters could cause security issues, pretend the subroutine Main is code with very low trust and CallTwo is your application code.

Listing 29.3 User-Filtered Exception Handlers and try / finally Blocks
 Imports System Public Class VBTest   Public Shared Sub Main     Try       Console.WriteLine("Try 1")       CallTwo     Catch When (RetTrue("1"))       Console.WriteLine("Catch 1")     Finally       Console.WriteLine("Finally 1")     End Try   End Sub   Public Shared Sub CallTwo     Try       Console.WriteLine("Try 2")       ThrowException     Finally       Console.WriteLine("Finally 2")     End Try   End Sub   Public Shared Sub ThrowException     Throw New Exception("Exception")   End Sub   Public Shared Function RetTrue(s as String) As Boolean     Console.WriteLine("Inside When " + s)     Return True   End Function End Class 

The console output from Listing 29.3 is printed in the order "Try 1," "Try 2," "Inside When 1," "Finally 2," "Catch 1," "Finally 1." The unexpected situation here is that the outer exception handling block gets to execute code at "Inside When 1" before the inner exception handling block runs the "Finally 2" clause.

There are two ways to fix a situation where your code has a try / finally scheme with a security problem. The first solution is to change your finally block to a catch block and rethrow the exception at the end of the catch block. The second solution is to wrap your try / finally blocks with another try block and add a catch block to the outer try that simply rethrows the exception.

Race Conditions

Race conditions in software are the cause of many bugs , both security related and non-security related . There are numerous ways that a race condition could cause a security problem. Rather than trying to compile a list that would probably be incomplete, my recommendation is that you make sure to plan for multithreaded callers in your application. In addition, you should write multithreaded tests for your application. If you aren't sure how to deal with multithreading in the .NET Framework, look for "multithreaded programs" in the index of the .NET Framework SDK.

for RuBoard


. NET Framework Security
.NET Framework Security
ISBN: 067232184X
EAN: 2147483647
Year: 2000
Pages: 235

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