File: src\Framework\MS\Internal\AppModel\AppSecurityManager.cs
Project: wpf\PresentationFramework.csproj (PresentationFramework)
//------------------------------------------------------------------------------
//  Microsoft Avalon
//  Copyright (c) Microsoft Corporation, 2001
//
//  File:           appsecuritymanager.cs
//
//  Description:    AppSecurityManager class.
//
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// IMPORTANT: We are creating an instance of IInternetSecurityManager here. This
// is currently also done in the CustomCredentialPolicy at the Core level. Any
// modification to either of these classes--especially concerning MapUrlToZone--
// should be considered for both classes.
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
//  History:        04-24-02 - Microsoft - created
//------------------------------------------------------------------------------
 
using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Security;
using System.Security.Permissions;
using Microsoft.Win32;
using System.IO.Packaging; 
using System.Windows;
using System.Windows.Interop;
using MS.Internal.Utility;
using MS.Win32; 
using System.Runtime.InteropServices;
using MS.Internal.Documents.Application;
 
using SecurityHelper=MS.Internal.SecurityHelper;
 
namespace MS.Internal.AppModel
{
 
    enum LaunchResult
    {
        Launched, 
        NotLaunched, 
        NotLaunchedDueToPrompt
    };
    
    internal static class AppSecurityManager
    {
        #region Internal Methods
 
 
        ///<summary> 
        ///     Safely launch the browser if you can. 
        ///     If you can't demand unmanaged code permisison. 
        ///
        ///     originatingUri = the current uri
        ///     destinationUri = the uri you are going to. 
        ///</summary> 
        ///<SecurityNote>
        /// Critical - gets access to critical resource (uri), calls critical code (launch browser)
        /// TreatAsSafe because 
        ///                     we consider navigates to http or http to the top-level browser as safe. 
        ///                     we consider navigates to mailto as safe. 
        ///                     for all other cases - we demand Unmanaged Code Permission 
        ///</SecurityNote>
        [SecurityCritical, SecurityTreatAsSafe]
        internal static void SafeLaunchBrowserDemandWhenUnsafe( Uri originatingUri, Uri destinationUri, bool fIsTopLevel ) 
        {
            LaunchResult launched = LaunchResult.NotLaunched; 
            
            launched = SafeLaunchBrowserOnlyIfPossible( originatingUri, destinationUri, fIsTopLevel ) ; 
            if ( launched == LaunchResult.NotLaunched ) 
            {
                SecurityHelper.DemandUnmanagedCode();
                UnsafeLaunchBrowser( destinationUri );
            }
        }
 
        ///<summary> 
        /// Safely launch the browser if it's possible to do so in partial trust
        /// Returns enum indicating whether we safely launched ( or at least think we did). 
        ///
        ///     This function is appropriate for use when we launch the browser from partial trust 
        ///     ( as it doesn't perform demands for the "unsafe" cases ) 
        ///</summary> 
        internal static LaunchResult SafeLaunchBrowserOnlyIfPossible(Uri originatingUri, Uri destinationUri, bool fIsTopLevel)
        {
            return SafeLaunchBrowserOnlyIfPossible(originatingUri, destinationUri, null, fIsTopLevel);
        }
        
        ///<summary> 
        /// Safely launch the browser if it's possible to do so in partial trust
        /// Returns enum indicating whether we safely launched ( or at least think we did). 
        /// Html target names can be passed in with this.
        ///     This function is appropriate for use when we launch the browser from partial trust 
        ///     ( as it doesn't perform demands for the "unsafe" cases ) 
        ///</summary> 
        ///<SecurityNote>
        /// Critical - gets access to critical resource (uri), calls critical code (launch browser)
        ///
        /// TreatAsSafe because 
        ///                     we consider navigates to http or http to the top-level browser as safe. 
        ///                     we consider navigates to mailto as safe. 
        ///
        ///                     for all other cases - we don't launch the browser - and return a result
        ///                     indicating that we didn't launch. 
        ///</SecurityNote>
        [SecurityCritical, SecurityTreatAsSafe]
        internal static LaunchResult SafeLaunchBrowserOnlyIfPossible(Uri originatingUri, Uri destinationUri, string targetName, bool fIsTopLevel )
        {
            LaunchResult launched = LaunchResult.NotLaunched ; 
 
            bool isKnownScheme =  (Object.ReferenceEquals(destinationUri.Scheme, Uri.UriSchemeHttp))  ||
                                   (Object.ReferenceEquals(destinationUri.Scheme, Uri.UriSchemeHttps)) ||
                                   destinationUri.IsFile;
 
            bool fIsMailTo = String.Compare(destinationUri.Scheme, Uri.UriSchemeMailto, StringComparison.OrdinalIgnoreCase) == 0 ; 
            
            // We elevate to navigate the browser iff: 
            //  We are user initiated AND
            //      Scheme == http/https & topLevel OR scheme == mailto.
            //
            // For all other cases ( evil protocols etc). 
            // We will demand. 
            //
            // The check of IsInitialViewerNavigation is necessary because viewer applications will probably
            // need to call Navigate on the URI they receive, but we want them to be able to do it in partial trust.
            if ((!BrowserInteropHelper.IsInitialViewerNavigation && 
                MS.Internal.PresentationFramework.SecurityHelper.CallerHasUserInitiatedNavigationPermission()) &&
                  ((fIsTopLevel && isKnownScheme) || fIsMailTo) )
            {
                if (isKnownScheme) 
                {
                    IBrowserCallbackServices ibcs = ( Application.Current != null ) ? Application.Current.BrowserCallbackServices : null ;
                    if (ibcs != null)
                    {
                        launched = CanNavigateToUrlWithZoneCheck(originatingUri , destinationUri); 
                        if ( launched == LaunchResult.Launched ) 
                        {
                            // resetting launched to NotLaunched here; if the assert succeeds
                            // and ibcs.DelegateNavigation does not throw then we will set it to Launched.
                            launched = LaunchResult.NotLaunched;
                            // Browser app.
                            // 
 
                            ibcs.DelegateNavigation( BindUriHelper.UriToString( destinationUri ), targetName, GetHeaders(destinationUri));   
 
                            launched = LaunchResult.Launched ; 
                        }                           
                    }
                }
                else if ( fIsMailTo ) // unnecessary if - but being paranoid. 
                {
                    // Shell-Exec the browser to the mailto url. 
                    // assumed safe - because we're only allowing this for mailto urls. 
                    // 
                    
                    UnsafeNativeMethods.ShellExecute( new HandleRef( null, IntPtr.Zero) , /*hwnd*/ 
                                                      null, /*operation*/ 
                                                      BindUriHelper.UriToString( destinationUri ), /*file*/
                                                      null, /*parameters*/ 
                                                      null, /*directory*/ 
                                                      0 ); /*nShowCmd*/ 
                    launched = LaunchResult.Launched ;  
                }
            }
 
            return launched ; 
        }
 
        // This invokes the browser unsafely.  
        // Whoever is calling this function should do the right demands.
        /// <SecurityNote>
        /// Critical - gets access to critical resource (uri and browsercallback services), calls critical code (launch browser)
        /// </SecurityNote>
        [SecurityCritical]
        internal static void UnsafeLaunchBrowser(Uri uri, string targetFrame = null)
        {
            // This'll likely go into SafeLaunchBrowser() function.
            if (Application.Current != null && Application.Current.CheckAccess())
            {
                IBrowserCallbackServices ibcs = Application.Current.BrowserCallbackServices;
                if (ibcs != null)
                {
                    // Browser app.
                    // 
 
                    ibcs.DelegateNavigation(BindUriHelper.UriToString(uri), targetFrame, GetHeaders(uri));
                    return;
                }
            }
 
            ShellExecuteDefaultBrowser(uri);
        }
 
        /// <summary>
        /// Opens the default browser for the passed in Uri.
        /// </summary>
        /// <SecurityNote>
        /// Critical - calls critical code (ShellExecuteEx)
        /// </SecurityNote>
        [SecurityCritical]
        internal static void ShellExecuteDefaultBrowser(Uri uri)
        {
            UnsafeNativeMethods.ShellExecuteInfo sei = new UnsafeNativeMethods.ShellExecuteInfo();
            sei.cbSize = Marshal.SizeOf(sei);
            sei.fMask = UnsafeNativeMethods.ShellExecuteFlags.SEE_MASK_FLAG_DDEWAIT;
            /*
            There is a bug on Windows Vista (with IE 7): ShellExecute via SEE_MASK_CLASSNAME fails for an 
            http[s]:// URL. It works fine for file://. The cause appears to be that the DDE command template
            defined in HKCR\IE.AssocFile.HTM\shell\opennew\ddeexec is used: [file://%1",-1,,,,,]. On XP, the
            the key used is (supposedly) HKCR\htmlfile\shell\opennew\ddeexec, and its value is ["%1",,-1,0,,,,].
            The workaround here is to add the SEE_MASK_CLASSNAME flag only for non-HTTP URLs. For HTTP, 
            "plain" ShellExecute just works, incl. with Firefox/Netscape as the default browser.
            */
            if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
            {
                sei.fMask |= UnsafeNativeMethods.ShellExecuteFlags.SEE_MASK_CLASSNAME;
                sei.lpClass = ".htm"; // The default browser is looked up by this.
            }
            sei.lpFile = uri.ToString(); // It's safe to use Uri.ToString since there's an inheritance demand on it that prevents spoofing by subclasses.
            if (!UnsafeNativeMethods.ShellExecuteEx(sei))
                throw new InvalidOperationException(SR.Get(SRID.FailToLaunchDefaultBrowser),
                    new System.ComponentModel.Win32Exception(/*uses the last Win32 error*/));
        }
 
        #endregion Internal Methods
 
        #region Private Methods
 
        /// <summary>
        /// Returns the HTTP "Referer" header. 
        /// </summary>
        /// <returns>returns a string containing one or more HTTP headers separated by \r\n; the string must also be terminated with a \r\n</returns>
        private static string GetHeaders(Uri destinationUri)
        {
            string referer = BindUriHelper.GetReferer(destinationUri);
 
            if (!String.IsNullOrEmpty(referer))
            {
                // The headers we pass in to IWebBrowser2.Navigate must
                // be terminated with a \r\n because the browser then
                // concatenates its own headers on to the end of that string.
                referer = RefererHeader + referer + "\r\n";
            }
 
            return referer;
        }
 
        // 
        // Functionally equivalent copy of Trident's CanNavigateToUrlWithZoneCheck function
        //  Checks to see whether a navigation is considered a zone elevation. 
        //  Once a zone elevation is identified - calls into urlmon to check settings. 
        // 
        ///<SecurityNote> 
        ///     Critical - performs elevations to call IsFeatureEnabled; call critical method MUTZ
        ///
        ///     TreatAsSafe - information disclosed is whether a navigation is "safe" or not. 
        ///                   considered ok to give out. you will be able to get this anyway by trapping exceptions
        ///                   or seeing whether a navigation succeeded/failed. 
        ///</SecurityNote> 
        [SecurityCritical, SecurityTreatAsSafe ] 
        private static LaunchResult CanNavigateToUrlWithZoneCheck( Uri originatingUri, Uri destinationUri ) 
        {
            LaunchResult launchResult = LaunchResult.NotLaunched ; // fail securely - assume this is the default. 
            int targetZone = NativeMethods.URLZONE_LOCAL_MACHINE ; // fail securely this is the most priveleged zone
            int sourceZone = NativeMethods.URLZONE_INTERNET ; // fail securely this is the least priveleged zone. 
            bool fEnabled = true;
            
            EnsureSecurityManager(); 
            
            // is this feature enabled ? 
            fEnabled  =  UnsafeNativeMethods.CoInternetIsFeatureEnabled(
                                        NativeMethods.FEATURE_ZONE_ELEVATION, 
                                        NativeMethods.GET_FEATURE_FROM_PROCESS) != NativeMethods.S_FALSE ;
 
            targetZone = MapUrlToZone(destinationUri);
 
            // Get source zone. 
 
            // Initialize sourceUri to null so that source zone defaults to the least privileged zone.
            Uri sourceUri = null;
 
            // If the MimeType is not a container, attempt to find sourceUri.
            // sourceUri should be null for Container cases, since it always assumes
            // the least privileged zone (InternetZone).
            if (Application.Current.MimeType != MimeType.Document)
            {
                sourceUri = BrowserInteropHelper.Source;
            }
            else if (destinationUri.IsFile && 
                System.IO.Path.GetExtension(destinationUri.LocalPath)
                    .Equals(DocumentStream.XpsFileExtension, StringComparison.OrdinalIgnoreCase))
            {
                // In this case we know the following:
                //  1) We are currently a Container
                //  2) The destination is a File and another Container
 
                // In this case we want to treat the destination as internet too so Container
                // can navigate to other Containers by passing zone checks
                targetZone = NativeMethods.URLZONE_INTERNET;
            }
 
            if ( sourceUri != null ) 
            {
                sourceZone = MapUrlToZone(sourceUri);
            }
            else
            {
                // 2 potential ways to get here. 
                //      a) We aren't a fusion hosted app. Assume full-trust. 
                //      b) Some bug in hosting caused source Uri to be null. 
                //
                // For a - we will say there is no cross-domain check.     
                //     b - we'll assume InternetZone, and use Source. 
 
                bool fTrusted = SecurityHelper.CheckUnmanagedCodePermission(); 
 
                if ( fTrusted ) 
                {
                    return LaunchResult.Launched;
                }
                else
                {
                    //
                    //  If we didn't get a SourceUri, we'll assume internet zone. 
                    //  And use Source for the uri of origin. 
                    //  
                    //  This isn't quite right - but the sourceUri is only used to show a message to the user. 
                    //  Worse case is confusing user experience. ( this uri is not used in the elevation determination). 
                    //
                
                    sourceZone = NativeMethods.URLZONE_INTERNET ;
                    sourceUri = originatingUri ; 
                }
            }
 
            // <Notes from Trident>
            // ------------------------------
            // Check if there is a zone elevation.
            // Custom zones would have a higher value that URLZONE_UNTRUSTED, so this solution isn't quite complete.
            // However, we don't know of any product actively using custom zones, so rolling this out.
            // INTRANET and TRUSTED are treated as equals.
            // ------------------------------
            // </Notes from Trident>
 
            //
            // Note the negative logic - it first sees to see something is *not* a zone elevation. 
            //
            if (
                    // Even if feature is disabled. 
                    // We still block navigation to local machine. 
                    // IF source zone is internet or restricted. 
            
                    (!fEnabled && 
                    ((sourceZone != NativeMethods.URLZONE_INTERNET &&
                       sourceZone != NativeMethods.URLZONE_UNTRUSTED) ||
                       targetZone != NativeMethods.URLZONE_LOCAL_MACHINE)) ||
 
                    // If feature is enabled
                    // It's not a zone elevation if
                    //     the zones are equal OR
                    //        the zones are both less than restricted and 
                    //            the sourceZone is more trusted than Target OR
                    //            sourceZone and TargetZone are both Intranet or Trusted
                    // 
                    // per aganjam - Intranet and Trusted are treated as equivalent 
                    // as it was a common scenario for IE. ( website on intranet points to trusted site). 
                    //
                    
                    (fEnabled &&
                    ( 
                        sourceZone == targetZone ||
                        (
                            sourceZone <= NativeMethods.URLZONE_UNTRUSTED && 
                            targetZone <= NativeMethods.URLZONE_UNTRUSTED && 
                            (
                                sourceZone < targetZone || 
                                ( 
                                    (sourceZone == NativeMethods.URLZONE_TRUSTED || sourceZone == NativeMethods.URLZONE_INTRANET) &&
                                    (targetZone == NativeMethods.URLZONE_TRUSTED || targetZone == NativeMethods.URLZONE_INTRANET) 
                                ) 
                            )
                        )
                        )))
            {
                // There is no zone elevation. You can launch away ! 
                return LaunchResult.Launched ;                
            }
            
            launchResult = CheckBlockNavigation( sourceUri , 
                                                  destinationUri, 
                                                  fEnabled ) ; 
 
            return launchResult ; 
        }
 
        ///<summary> 
        ///     Called when we suspect there is a zone elevation. 
        ///     Calls the Urlmon IsFeatureZoneElevationEnabled which may pop UI based on settings. 
        /// functionally equivalent to the BlockNavigation: label in Trident's CanNavigateToUrlWithZoneCheck    
        ///</summary> 
        ///<SecurityNote> 
        ///     Critical - calls a function that has a SUC on it. ( CoIntenrnetIsFeatureZoneElevationEnabled)
        ///</SecurityNote> 
        [SecurityCritical] 
        private static LaunchResult CheckBlockNavigation(Uri originatingUri, Uri destinationUri, bool fEnabled )
        {
 
            if (fEnabled)
            {
                if( UnsafeNativeMethods.CoInternetIsFeatureZoneElevationEnabled( 
                            BindUriHelper.UriToString( originatingUri ) , 
                            BindUriHelper.UriToString( destinationUri ) , 
                            _secMgr, 
                            NativeMethods.GET_FEATURE_FROM_PROCESS) == NativeMethods.S_FALSE)
                {
                    return LaunchResult.Launched;
                }
                else 
                {
                    if ( IsZoneElevationSettingPrompt( destinationUri ) ) 
                    {
                        // url action is query, and we got a "no" answer back. 
                        // we can assume user responded No. 
                        return LaunchResult.NotLaunchedDueToPrompt ;
                    }
                    else
                        return LaunchResult.NotLaunched ;                    
                }
            }
            else
            {
                return LaunchResult.Launched ;
            }        
        }
 
        // Is ZoneElevation setting set to prompt ?     
        ///<SecurityNote> 
        ///     Critical - elevates to call ProcessUrlAction. 
        ///     TreatAsSafe - information return indicates whether we will prompt for the current zone. 
        ///                   considered ok to expose. 
        ///</SecurityNote> 
        [SecurityCritical, SecurityTreatAsSafe]
        private static bool IsZoneElevationSettingPrompt( Uri target ) 
        {
            Invariant.Assert(_secMgr != null ); 
            
            // Was this due to a prompt ? 
 
            int policy = NativeMethods.URLPOLICY_DISALLOW ; 
 
            unsafe
            {
                String targetString = BindUriHelper.UriToString( target ) ;
                new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Assert(); // BlessedAssert: 
                try
                {
                    _secMgr.ProcessUrlAction( targetString, 
                                              NativeMethods.URLACTION_FEATURE_ZONE_ELEVATION ,
                                              (byte*) & policy , 
                                              Marshal.SizeOf( typeof (int) ), 
                                              null, 
                                              0,
                                              NativeMethods.PUAF_NOUI, 
                                              0 ); 
                }
                finally
                {
                    CodeAccessPermission.RevertAssert(); 
                }                
            }
            
            return ( policy == NativeMethods.URLPOLICY_QUERY ) ; 
        }
        
        ///<SecurityNote> 
        /// Critical - elevates to call unmanaged code to set the security site. 
        ///     The SecurityManager is a critical resource.
        /// Safe: The Security Manager is used only within this class (not exposed). Just creating it has 
        ///     no observable side effects.
        ///</SecurityNote> 
        [SecurityCritical, SecurityTreatAsSafe]
        private static void EnsureSecurityManager()
        {
            // IMPORTANT: See comments in header r.e. IInternetSecurityManager
 
            if( _secMgr == null ) 
            {
                lock( _lockObj ) 
                {      
                    if ( _secMgr == null ) // null check again - now that we're in the lock. 
                    {
                          _secMgr = (UnsafeNativeMethods.IInternetSecurityManager) new InternetSecurityManager();                 
 
                          //
                         // Set the Security Manager Site. 
                         // This enables any dialogs popped to be modal to our window. 
                         // 
                        _secMgrSite = new SecurityMgrSite();                 
                        new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Assert(); // BlessedAssert: 
                        try
                        {
                             _secMgr.SetSecuritySite( (NativeMethods.IInternetSecurityMgrSite) _secMgrSite ) ; 
                        }
                        finally
                        {
                            CodeAccessPermission.RevertAssert(); 
                        }                       
                    }                    
                }
            }                
        }
 
 
        ///<SecurityNote> 
        /// Critical - elevates to call SetSecuritySite. 
        /// TreatAsSafe - clearing the security site is considered safe. 
        ///               worse that can happen is any urlmon prompts will be non-modal. 
        ///</SecurityNote> 
        
        [SecurityCritical, SecurityTreatAsSafe ]
        internal static void ClearSecurityManager()
        {
            if (_secMgr != null ) 
            {
                new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Assert();  // BlessedAssert: 
                try
                {
                    lock ( _lockObj ) 
                    {
                        if ( _secMgr != null ) 
                        {
                            _secMgr.SetSecuritySite( null ) ; 
                            _secMgrSite = null ;                 
                            _secMgr = null ;                     
                        }
                    }
                }
                finally
                {
                    CodeAccessPermission.RevertAssert(); 
                }
                
 
            }
        }
 
        ///<SecurityNote> 
        /// Critical - Calls the COM method. A URL's security zone is not a big secret, and in most cases it 
        ///     can be inferred by just parsing the URL, but it's still information obtained under elevation,
        ///     and thus we shouldn't leak it without a good reason.
        ///</SecurityNote> 
        [SecurityCritical]
        internal static int MapUrlToZone(Uri url)
        {
            EnsureSecurityManager();
            int zone;
            _secMgr.MapUrlToZone(BindUriHelper.UriToString(url), out zone, 0);
            return zone;
        }
        
        [ComImport, ComVisible(false), Guid("7b8a2d94-0ac9-11d1-896c-00c04Fb6bfc4")]
        internal class InternetSecurityManager {
 
        }
 
 
 
        #endregion Private Methods
 
        #region Private Fields
 
        private const string RefererHeader = "Referer: ";
 
        private const string BrowserOpenCommandLookupKey = "htmlfile\\shell\\open\\command";
 
        // Object to be used for locking.  Using typeof(Util) causes an FxCop
        // violation DoNotLockOnObjectsWithWeakIdentity
        private static object _lockObj = new object();
 
        ///<SecurityNote> 
        ///     Critical - requires an elevation to create. 
        ///</SecurityNote> 
        [SecurityCritical]
        private static UnsafeNativeMethods.IInternetSecurityManager _secMgr ; 
        
        private static SecurityMgrSite _secMgrSite ; 
        
        #endregion Private Fields
    }
}