File: MS\Internal\AppModel\DefaultCredentialsZonePolicy.cs
Web Access
Project: src\wpf\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// Security note
//   Mitigation: see DefaultCredentialsZonePolicy / PackageBoundaryGuard
//   Kill switches (default off):
//     Switch.System.Windows.Net.DoNotApplyZoneCheckForDefaultCredentials
//     Switch.System.Windows.Documents.DisableXpsPackageBoundaryEnforcement
//   Do NOT remove the gate logic without security review.
 
//
// Description:
//
// Helper that decides whether default (NTLM/Kerberos/Negotiate) credentials
// should be attached to an outgoing WebRequest, based on the URL Security Zone
// of the target URI.
//
// Historical note:
//   On .NET Framework, WPF used to register an ICredentialPolicy with
//   System.Net.AuthenticationManager.CredentialPolicy. The framework's auth
//   client modules (NTLM/Kerberos/Negotiate/Basic/Digest) would consult that
//   policy when responding to a 401 challenge and, for Internet/Untrusted
//   zones, suppress the credentials. That mechanism is implemented by
//   <see cref="CustomCredentialPolicy"/>, which is preserved in source for
//   backward-compatibility purposes (selected via the AppContext switch
//   "Switch.System.Windows.Net.DoNotApplyZoneCheckForDefaultCredentials").
//
//   On .NET 5+, AuthenticationManager.CredentialPolicy is obsolete (SYSLIB0009)
//   and is a runtime no-op: the new HttpClient-based pipeline never consults
//   it. Registering a policy therefore does nothing, which silently regressed
//   the original safety behavior.
//
//   To restore .NET Framework parity, callers can perform the zone check
//   inline before enabling UseDefaultCredentials by calling
//   ShouldSendDefaultCredentials below. This is the default behavior; the
//   AppContext switch above can be set to true to opt out and restore the
//   previous (no-op) registration path as a compatibility escape hatch.
//
// Policy:
//   Allow default credentials to flow only to Local Machine / Intranet /
//   Trusted zones. Block them for Internet / Untrusted zones.
//
//   On modern Windows installs without Internet Explorer/Edge zone configuration
//   (and in some service / lockdown contexts), IInternetSecurityManager.MapUrlToZone
//   can return URLZONE_INTERNET even for loopback (127.0.0.1, ::1) and RFC1918
//   private addresses. To preserve the documented intent and avoid silently
//   regressing intranet/loopback authentication scenarios, an explicit IP-based
//   pre-check is performed for definitively non-routable destinations
//   (loopback / RFC1918 / link-local / IPv6 link-local & ULA) before falling
//   back to MapUrlToZone. The pre-check can only relax the decision in cases
//   where the destination is provably non-public; it never widens trust to a
//   public IP, so the Internet-zone leak protection is preserved.
//
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// IMPORTANT: We are creating an instance of IInternetSecurityManager here. This
// is currently also done in the AppSecurityManager at the Framework level and
// in CustomCredentialPolicy. Any modification to either of these classes--
// especially concerning MapUrlToZone--should be considered for both classes.
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// An IInternetSecurityManagerSite is not currently needed here, because the
// only method of IInternetSecurityManager that we are calling is MapUrlToZone,
// and that does not prompt the user.
//
 
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
 
using MS.Internal.PresentationCore;
using MS.Win32;
 
namespace MS.Internal.AppModel
{
    internal static class DefaultCredentialsZonePolicy
    {
        static DefaultCredentialsZonePolicy()
        {
            _lockObj = new object();
        }
 
        /// <summary>
        /// Returns true if it is safe to attach default (NTLM/Kerberos/Negotiate)
        /// credentials to a request targeted at <paramref name="uri"/>.
        ///
        /// Restores the .NET Framework behavior that used to be provided by the
        /// now-obsolete <c>AuthenticationManager.CredentialPolicy</c> mechanism
        /// (SYSLIB0009): default credentials are only allowed to flow to URIs
        /// in the Local Machine, Intranet or Trusted security zones.
        /// </summary>
        internal static bool ShouldSendDefaultCredentials(Uri uri)
        {
            // Fail-closed on null or non-absolute URIs.
            //
            // Scope: this gate is only consulted from WpfWebRequestHelper.CreateRequest
            // and only inside the `if (httpRequest != null)` branch - i.e. exclusively
            // for HttpWebRequest/HttpsWebRequest targets. Non-HTTP schemes never reach
            // this method:
            //   * UNC paths (\\server\share\...) flow through FileWebRequest, so
            //     UseDefaultCredentials handling on UNC is unchanged by this gate.
            //   * file://, ftp://, pack:// likewise never produce an HttpWebRequest.
            //   * Relative URIs cannot reach WebRequest.Create at all - that API
            //     throws on relative input - so any null/non-absolute URI here is
            //     an unexpected/degenerate state and we deliberately refuse the
            //     automatic credential handshake rather than silently allow it.
            //
            // "Fail closed" here only suppresses the *automatic* NTLM/Kerberos/
            // Negotiate handshake on a 401 challenge. Explicit credentials set by
            // the caller via WebRequest.Credentials still flow normally and
            // continue to honor any server challenge.
            if (uri == null || !uri.IsAbsoluteUri)
            {
                return false;
            }
 
            // Explicit pre-check: if every IP the host resolves to is non-routable
            // (loopback / RFC1918 / link-local / IPv6 link-local or ULA), permit
            // credentials. This honors the documented Local Machine / Intranet
            // intent even when IInternetSecurityManager is degraded (e.g. modern
            // Windows installs without IE/Edge zone configuration).
            if (AllResolvedAddressesAreLocalOrPrivate(uri.DnsSafeHost))
            {
                return true;
            }
 
            switch (MapUrlToZone(uri))
            {
                // Always safe to send default credentials to these zones
                case NativeMethods.URLZONE_LOCAL_MACHINE:
                case NativeMethods.URLZONE_INTRANET:
                case NativeMethods.URLZONE_TRUSTED:
                    return true;
 
                // Never send default credentials to these zones (or any unknown zone)
                case NativeMethods.URLZONE_INTERNET:
                case NativeMethods.URLZONE_UNTRUSTED:
                default:
                    return false;
            }
        }
 
        // Per-host result cache. The decision for a given hostname is stable for the
        // lifetime of the process barring DNS changes; caching avoids a synchronous
        // DNS lookup on every outgoing request.
        private static readonly ConcurrentDictionary<string, bool> s_localHostCache =
            new(StringComparer.OrdinalIgnoreCase);
 
        /// <summary>
        /// Returns true when every IP address that <paramref name="host"/> resolves
        /// to is non-routable: IPv4 loopback (127.0.0.0/8), IPv4 link-local
        /// (169.254.0.0/16), RFC1918 private space (10/8, 172.16/12, 192.168/16),
        /// IPv6 loopback (::1), IPv6 link-local (fe80::/10) or IPv6 unique-local
        /// addresses (fc00::/7).
        /// </summary>
        /// <remarks>
        /// Used as a pre-check before IInternetSecurityManager.MapUrlToZone so
        /// loopback/Intranet decisions are reliable even on modern Windows installs
        /// where the zone manager is degraded (no IE/Edge zone configuration).
        /// DNS failures resolve to "not local/private" and fail closed in the caller.
        /// Results are cached per host for the lifetime of the process.
        /// </remarks>
        private static bool AllResolvedAddressesAreLocalOrPrivate(string host)
        {
            if (string.IsNullOrEmpty(host))
            {
                return false;
            }
 
            return s_localHostCache.GetOrAdd(host, static h =>
            {
                IPAddress[] addresses;
                if (IPAddress.TryParse(h, out IPAddress literal))
                {
                    addresses = new[] { literal };
                }
                else
                {
                    try
                    {
                        addresses = Dns.GetHostAddresses(h);
                    }
                    catch
                    {
                        // DNS failure: do not relax the policy. Fall through to
                        // MapUrlToZone (which will deny by default for unknown zones).
                        return false;
                    }
                }
 
                if (addresses == null || addresses.Length == 0)
                {
                    return false;
                }
 
                foreach (IPAddress a in addresses)
                {
                    if (!IsLocalOrPrivate(a))
                    {
                        return false;
                    }
                }
                return true;
            });
        }
 
        private static bool IsLocalOrPrivate(IPAddress address)
        {
            if (IPAddress.IsLoopback(address))
            {
                return true;
            }
 
            if (address.AddressFamily == AddressFamily.InterNetwork)
            {
                byte[] b = address.GetAddressBytes();
                // 10.0.0.0/8
                if (b[0] == 10) return true;
                // 172.16.0.0/12
                if (b[0] == 172 && (b[1] & 0xF0) == 16) return true;
                // 192.168.0.0/16
                if (b[0] == 192 && b[1] == 168) return true;
                // 169.254.0.0/16 (link-local)
                if (b[0] == 169 && b[1] == 254) return true;
            }
            else if (address.AddressFamily == AddressFamily.InterNetworkV6)
            {
                if (address.IsIPv6LinkLocal) return true;
                if (address.IsIPv6SiteLocal) return true;
                // Unique Local Address fc00::/7
                byte[] b = address.GetAddressBytes();
                if ((b[0] & 0xFE) == 0xFC) return true;
            }
 
            return false;
        }
 
        // Sentinel returned by MapUrlToZone when the underlying
        // IInternetSecurityManager could not be created (e.g. exotic Windows SKUs
        // without urlmon registration). The switch in ShouldSendDefaultCredentials
        // treats any non-trusted zone (including this sentinel) as "do not send
        // default credentials", so callers automatically fail closed.
        private const int URLZONE_UNAVAILABLE = -1;
 
        internal static int MapUrlToZone(Uri uri)
        {
            EnsureSecurityManager();
 
            UnsafeNativeMethods.IInternetSecurityManager sm = _securityManager;
            if (sm == null)
            {
                // Initialization previously failed; fail closed.
                return URLZONE_UNAVAILABLE;
            }
 
            sm.MapUrlToZone(BindUriHelper.UriToString(uri), out int targetZone, 0);
            return targetZone;
        }
 
        private static void EnsureSecurityManager()
        {
            // IMPORTANT: See comments in header r.e. IInternetSecurityManager
 
            if (_securityManager != null || _securityManagerInitFailed)
            {
                return;
            }
 
            lock (_lockObj)
            {
                if (_securityManager != null || _securityManagerInitFailed)
                {
                    return;
                }
 
                try
                {
                    _securityManager = (UnsafeNativeMethods.IInternetSecurityManager)new InternetSecurityManager();
                }
                catch (Exception)
                {
                    // CoCreateInstance failure on exotic Windows SKUs (e.g. Server
                    // Core / Nano Server / future trimmed images without urlmon
                    // registration). Mark as failed so we do not pay the cost of
                    // attempting recreation on every request, and let MapUrlToZone
                    // fail closed -- the caller will refuse to attach default
                    // credentials, which preserves the security guarantee.
                    _securityManagerInitFailed = true;
                }
            }
        }
 
        [ComImport, ComVisible(false), Guid("7b8a2d94-0ac9-11d1-896c-00c04Fb6bfc4")]
        private class InternetSecurityManager
        {
        }
 
        private static UnsafeNativeMethods.IInternetSecurityManager _securityManager;
 
        private static volatile bool _securityManagerInitFailed;
 
        private static object _lockObj;
    }
}