Safe Impersonation With Whidbey


Over the last couple of days we’ve talked about how to impersonate another user, and some security issues to keep in mind while impersonating.  Now I’d like to take a look at some new features available in Whidbey which can make the whole process much nicer.  I’m going to code this up in Visual Basic to take advantage of VB’s ability to provide exception filters, which allow me to undo impersonation on the first pass of exception handling without actually catching the exception.

To start with, I’m going to make a SafeHandle wrapper around the user token, so that I gain all the benefits presented by the new SafeHandle model.  (More information on SafeHandles here and here).

Friend NotInheritable Class SafeUserToken
    Inherits SafeHandle

    <SuppressUnmanagedCodeSecurity()> _
    <ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)> _
    Private Declare Function CloseHandle Lib “Kernel32” (ByVal hObject As IntPtr) As Boolean

    Private Sub New()
        MyBase.New(IntPtr.Zero, True)
    End Sub

    Public Overrides ReadOnly Property IsInvalid() As Boolean
        Get
            Return IntPtr.Zero.Equals(handle)
        End Get
    End Property

    Protected Overrides Function ReleaseHandle() As Boolean
        Return CloseHandle(handle)
    End Function
End Class

Now, I’ll define a delegate that will be called under the impersonation context.  The delegate takes a single parameter which is a generic type.  The return value will also be generic.

Public Delegate Function ImpersonationWorkFunction(Of TReturn, TParameter)(ByVal paramter As TParameter) As TReturn

For the actual work of the utility library, I’m going to have a static method that takes:

  • the user name
  • domain
  • password (As a SecureString in order to take advantage of all the extra security benefits it provides)
  • logon type
  • logon provider
  • a delegate to run in the impersonated context
  • and a parameter to pass to the delegate

The return value of the method will be whatever the delegate returns.  Essentially this method is a wrapper around the code exposed by the delegate which makes it run while impersonating.  The code for this method is a pretty straightforward fallout of our last post.

<SecurityPermission(SecurityAction.Demand, UnmanagedCode := True)> _
Public Shared Function Impersonate(Of TReturn, TParameter)(ByVal userName As StringByVal domain As String, _
  ByVal password As SecureString, ByVal parameter As TParameter, _
  ByVal impersonationWork As ImpersonationWorkFunction(Of TReturn, TParameter), _
  ByVal logonMethod As LogonType, ByVal provider As LogonProvider) As TReturn
    ‘ Check the parameters
    If String.IsNullOrEmpty(userName) Then
        Throw New ArgumentNullException(“userName”)
    End If
    If password Is Nothing Then
        Throw New ArgumentNullException(“password”)
    End If
    If impersonationWork = Nothing Then
        Throw New ArgumentNullException(“impersonationWork”)
    End If
    If logonMethod < LogonType.Interactive Or LogonType.NewCredentials < logonMethod Then
        Throw New ArgumentOutOfRangeException(“logonMethod”)
    End If
    If provider < LogonProvider.DefaultProvider Or LogonProvider.WinNT50 < provider Then
        Throw New ArgumentOutOfRangeException(“provider”)
    End If

    Dim passwordPointer As IntPtr = IntPtr.Zero
    Dim token As SafeUserToken = Nothing
    Dim context As WindowsImpersonationContext = Nothing

    Try
        ‘ convert the password to a unicode string
        passwordPointer = Marshal.SecureStringToGlobalAllocUnicode(password)

        ‘ get a user token
        If Not LogonUserW(userName, domain, passwordPointer, logonMethod, provider, token) Then
            Throw New Win32Exception(Err.LastDllError)
        End If
    Finally
        ‘ Erase the memory that the password was stored in
        If Not IntPtr.Zero.Equals(passwordPointer) Then
            Marshal.ZeroFreeGlobalAllocUnicode(passwordPointer)
        End If
    End Try

    Try
        ‘ Impersonate
        Debug.Assert(token IsNot Nothing)
        context = WindowsIdentity.Impersonate(token.DangerousGetHandle())

        ‘ Call out to the work function
        Return impersonationWork(parameter)
    Catch When UndoImpersonation(token, context) = False
        Debug.Assert(False, “UndoImpersonation returned False”)
    Finally
        UndoImpersonation(token, context)
    End Try
End Function

Private Shared Function UndoImpersonation(ByRef token As SafeUserToken, ByRef context As WindowsImpersonationContext) As Boolean
    If context IsNot Nothing Then
        context.Undo()
        context = Nothing
    End If
    If token IsNot Nothing Then
        token.Dispose()
        token = Nothing
    End If

    Return True
End Function

Basically, we start by calling LogonUser.  Then we begin impersonating and call the delegate.  If the delegate were to throw, we undo the impersonation in the exception filter.  If it doesn’t throw, the finally block takes care of undoing impersonation.

Finally, this method would be made slightly easier to use if we provided an overload that defaulted the logon type to Interactive and the logon provider to DefaultProvider.

Public Shared Function Impersonate(Of TReturn, TParameter)(ByVal userName As StringByVal domain As String, _
 ByVal password As SecureString, ByVal parameter As TParameter, _
 ByVal impersonationWork As ImpersonationWorkFunction(Of TReturn, TParameter)) As TReturn

    Return Impersonate(userName,  _
      domain, _
      password, _
      parameter, _
      impersonationWork, _
      LogonType.Interactive, _
      LogonProvider.DefaultProvider)
End Function

We can compile all of that infrastructure into a utility library.  Using that library will make safely completing some work in an impersonated context pretty easy, especially when you take advantage of C#’s new support for anonymous delegates:

WindowsImpersonation.Impersonate<objectobject>(“SomeOtherUser”, “MYDOMAIN”, password, nulldelegate
{
    Console.WriteLine(“Impersonating {0}”, WindowsIdentity.GetCurrent().Name);
    return null;
});

Perhaps a more real-world usage might be something like this:

string fileContents =
WindowsImpersonation.Impersonate<stringstring>(“AdminUser”, “MYDOMAIN”, password, @”c:\AdminStuff\File.txt”, delegate(string file)
{
    using(StreamReader fileReader = new StreamReader(file))
        return fileReader.ReadToEnd();
});

Comments (7)

  1. Nicole Calinoiu says:

    While the use of a delegate is certainly convenient for this sort of thing, it’s potentially quite dangerous given that there’s no guarantee that the caller actually chose the delegate implementation. (See http://blogs.msdn.com/ptorr/archive/2005/01/16/353816.aspx for a discussion by Peter Torr of a somewhat similar problem.) Since it’s not possible to place a demand for SecurityPermissionUnmanagedCode on the delegate itself, this is more than a little tricky. Providing a wrapper that enables a demand when setting the delegate could help, but a naive caller would still be creating a security hole if it fails to use the wrapper as intended.

    Constraining the delegate actions via use of PermitOnly would also be potentially helpful, but one would need to receive the accepted permission set from the caller in order to allow adequate flexibility, and the same "naive caller" problem rears its ugly head again. In addition, this approach would be completely useless when resources used by a particular implementation of the delegate are not protected by CAS.

    Unfortunately, I just can’t see any way to make this sort of thing safe for potentially naive callers. Unless there’s some wonderful technique I’m missing, it would seem prudent to make this functionality accessible by only one’s own code.

  2. Well, there’s really no good way to ensure that the caller uses the class library as intended. The goal of the code isn’t to allow arbitrary delegates to run in the impersonation context, but rather to be a wrapper around the high trust code which ensures that the impersonation is undone when the work completes.

    -Shawn

  3. Gavin says:

    Hey,

    I’m writing an application that requires me to authenticate as windows user account user to perform part of a process before resetting back to it’s original context.

    Unfortunately I have no choice but to use C# exclusively. Your code uses some language specific classes that and so I am unable to directly translate into C#.

    Is there a C# version available?

    Many Thanks

  4. Anne says:

    Hi!

    Could you please post your definition of the LogonUserW method?

    I'm wondering how the definition must look like to allow SafeHandle instances as parameter 'phToken'.

    Thanks a lot!

  5. Sure – you can find one here: clrsecurity.codeplex.com/…/47833

    Just make sure you're passing the safe handle as an out parameter of the declaration.

    -Shawn