//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Net; using System.Net.Cache; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using Validation; /// /// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote /// server leaving dangling connections, sending too much data, causing requests against /// internal servers, etc. /// /// /// Protections include: /// * Conservative maximum time to receive the complete response. /// * Only HTTP and HTTPS schemes are permitted. /// * Internal IP address ranges are not permitted: 127.*.*.*, 1::* /// * Internal host names are not permitted (periods must be found in the host name) /// If a particular host would be permitted but is in the blacklist, it is not allowed. /// If a particular host would not be permitted but is in the whitelist, it is allowed. /// public class UntrustedWebRequestHandler : DelegatingHandler { /// /// The set of URI schemes allowed in untrusted web requests. /// private ICollection allowableSchemes = new List { "http", "https" }; /// /// The collection of blacklisted hosts. /// private ICollection blacklistHosts = new List(Configuration.BlacklistHosts.KeysAsStrings); /// /// The collection of regular expressions used to identify additional blacklisted hosts. /// private ICollection blacklistHostsRegex = new List(Configuration.BlacklistHostsRegex.KeysAsRegexs); /// /// The collection of whitelisted hosts. /// private ICollection whitelistHosts = new List(Configuration.WhitelistHosts.KeysAsStrings); /// /// The collection of regular expressions used to identify additional whitelisted hosts. /// private ICollection whitelistHostsRegex = new List(Configuration.WhitelistHostsRegex.KeysAsRegexs); /// /// The maximum redirections to follow in the course of a single request. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] private int maxAutomaticRedirections = Configuration.MaximumRedirections; /// /// A value indicating whether to automatically follow redirects. /// [DebuggerBrowsable(DebuggerBrowsableState.Never)] private bool allowAutoRedirect = true; /// /// Initializes a new instance of the class. /// /// /// The inner handler. This handler will be modified to suit the purposes of this wrapping handler, /// and should not be used independently of this wrapper after construction of this object. /// public UntrustedWebRequestHandler(WebRequestHandler innerHandler = null) : base(innerHandler ?? new WebRequestHandler()) { // If SSL is required throughout, we cannot allow auto redirects because // it may include a pass through an unprotected HTTP request. // We have to follow redirects manually. // It also allows us to ignore HttpWebResponse.FinalUri since that can be affected by // the Content-Location header and open security holes. this.MaxAutomaticRedirections = Configuration.MaximumRedirections; this.InnerWebRequestHandler.AllowAutoRedirect = false; if (Debugger.IsAttached) { // Since a debugger is attached, requests may be MUCH slower, // so give ourselves huge timeouts. this.InnerWebRequestHandler.ReadWriteTimeout = (int)TimeSpan.FromHours(1).TotalMilliseconds; } else { this.InnerWebRequestHandler.ReadWriteTimeout = (int)Configuration.ReadWriteTimeout.TotalMilliseconds; } } /// /// Initializes a new instance of the class /// for use in unit testing. /// /// /// The inner handler which is responsible for processing the HTTP response messages. /// This handler should NOT automatically follow redirects. /// internal UntrustedWebRequestHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } /// /// Gets or sets a value indicating whether all requests must use SSL. /// /// /// true if SSL is required; otherwise, false. /// public bool IsSslRequired { get; set; } /// /// Gets or sets the total number of redirections to allow on any one request. /// Default is 10. /// public int MaxAutomaticRedirections { get { return this.InnerHandler is WebRequestHandler ? this.InnerWebRequestHandler.MaxAutomaticRedirections : this.maxAutomaticRedirections; } set { Requires.Range(value >= 0, "value"); this.maxAutomaticRedirections = value; if (this.InnerHandler is WebRequestHandler) { this.InnerWebRequestHandler.MaxAutomaticRedirections = value; } } } /// /// Gets or sets a value indicating whether to automatically follow redirects. /// public bool AllowAutoRedirect { get { return this.InnerHandler is WebRequestHandler ? this.InnerWebRequestHandler.AllowAutoRedirect : this.allowAutoRedirect; } set { this.allowAutoRedirect = value; if (this.InnerHandler is WebRequestHandler) { this.InnerWebRequestHandler.AllowAutoRedirect = value; } } } /// /// Gets or sets the time (in milliseconds) allowed to wait for single read or write operation to complete. /// Default is 500 milliseconds. /// public int ReadWriteTimeout { get { return this.InnerWebRequestHandler.ReadWriteTimeout; } set { this.InnerWebRequestHandler.ReadWriteTimeout = value; } } /// /// Gets a collection of host name literals that should be allowed even if they don't /// pass standard security checks. /// [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")] public ICollection WhitelistHosts { get { return this.whitelistHosts; } } /// /// Gets a collection of host name regular expressions that indicate hosts that should /// be allowed even though they don't pass standard security checks. /// [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")] public ICollection WhitelistHostsRegex { get { return this.whitelistHostsRegex; } } /// /// Gets a collection of host name literals that should be rejected even if they /// pass standard security checks. /// public ICollection BlacklistHosts { get { return this.blacklistHosts; } } /// /// Gets a collection of host name regular expressions that indicate hosts that should /// be rejected even if they pass standard security checks. /// public ICollection BlacklistHostsRegex { get { return this.blacklistHostsRegex; } } /// /// Gets the inner web request handler. /// /// /// The inner web request handler. /// public WebRequestHandler InnerWebRequestHandler { get { return (WebRequestHandler)this.InnerHandler; } } /// /// Gets the configuration for this class that is specified in the host's .config file. /// private static UntrustedWebRequestElement Configuration { get { return DotNetOpenAuthSection.Messaging.UntrustedWebRequest; } } /// /// Creates an HTTP client that uses this instance as an HTTP handler. /// /// The initialized instance. public HttpClient CreateClient() { var client = new HttpClient(this); client.MaxResponseContentBufferSize = Configuration.MaximumBytesToRead; if (Debugger.IsAttached) { // Since a debugger is attached, requests may be MUCH slower, // so give ourselves huge timeouts. client.Timeout = TimeSpan.FromHours(1); } else { client.Timeout = Configuration.Timeout; } return client; } /// /// Determines whether an exception was thrown because of the remote HTTP server returning HTTP 417 Expectation Failed. /// /// The caught exception. /// /// true if the failure was originally caused by a 417 Exceptation Failed error; otherwise, false. /// internal static bool IsExceptionFrom417ExpectationFailed(Exception ex) { while (ex != null) { WebException webEx = ex as WebException; if (webEx != null) { HttpWebResponse response = webEx.Response as HttpWebResponse; if (response != null) { if (response.StatusCode == HttpStatusCode.ExpectationFailed) { return true; } } } ex = ex.InnerException; } return false; } /// /// Send an HTTP request as an asynchronous operation. /// /// The HTTP request message to send. /// The cancellation token to cancel operation. /// /// Returns .The task object representing the asynchronous operation. /// protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { this.EnsureAllowableRequestUri(request.RequestUri); // Since we may require SSL for every redirect, we handle each redirect manually // in order to detect and fail if any redirect sends us to an HTTP url. // We COULD allow automatic redirect in the cases where HTTPS is not required, // but our mock request infrastructure can't do redirects on its own either. Uri originalRequestUri = request.RequestUri; int i; for (i = 0; i < this.MaxAutomaticRedirections; i++) { this.EnsureAllowableRequestUri(request.RequestUri); var response = await base.SendAsync(request, cancellationToken); if (this.AllowAutoRedirect) { if (response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.RedirectMethod || response.StatusCode == HttpStatusCode.RedirectKeepVerb) { // We have no copy of the post entity stream to repeat on our manually // cloned HttpWebRequest, so we have to bail. ErrorUtilities.VerifyProtocol( request.Method != HttpMethod.Post, MessagingStrings.UntrustedRedirectsOnPOSTNotSupported); Uri redirectUri = new Uri(request.RequestUri, response.Headers.Location); request = request.Clone(); request.RequestUri = redirectUri; continue; } } if (response.StatusCode == HttpStatusCode.ExpectationFailed) { // Some OpenID servers doesn't understand the Expect header and send 417 error back. // If this server just failed from that, alter the ServicePoint for this server // so that we don't send that header again next time (whenever that is). // "Expect: 100-Continue" HTTP header. (see Google Code Issue 72) // We don't want to blindly set all ServicePoints to not use the Expect header // as that would be a security hole allowing any visitor to a web site change // the web site's global behavior when calling that host. // TODO 5.0: verify that this still works in DNOA 5.0 var servicePoint = ServicePointManager.FindServicePoint(request.RequestUri); Logger.Http.InfoFormat( "HTTP POST to {0} resulted in 417 Expectation Failed. Changing ServicePoint to not use Expect: Continue next time.", request.RequestUri); servicePoint.Expect100Continue = false; } return response; } throw ErrorUtilities.ThrowProtocol(MessagingStrings.TooManyRedirects, originalRequestUri); } /// /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1". /// /// The ip address to check. /// /// true if this is a loopback IP address; false otherwise. /// private static bool IsIPv6Loopback(IPAddress ip) { Requires.NotNull(ip, "ip"); byte[] addressBytes = ip.GetAddressBytes(); for (int i = 0; i < addressBytes.Length - 1; i++) { if (addressBytes[i] != 0) { return false; } } if (addressBytes[addressBytes.Length - 1] != 1) { return false; } return true; } /// /// Determines whether the given host name is in a host list or host name regex list. /// /// The host name. /// The list of host names. /// The list of regex patterns of host names. /// /// true if the specified host falls within at least one of the given lists; otherwise, false. /// private static bool IsHostInList(string host, ICollection stringList, ICollection regexList) { Requires.NotNullOrEmpty(host, "host"); Requires.NotNull(stringList, "stringList"); Requires.NotNull(regexList, "regexList"); foreach (string testHost in stringList) { if (string.Equals(host, testHost, StringComparison.OrdinalIgnoreCase)) { return true; } } foreach (Regex regex in regexList) { if (regex.IsMatch(host)) { return true; } } return false; } /// /// Determines whether a given host is whitelisted. /// /// The host name to test. /// /// true if the host is whitelisted; otherwise, false. /// private bool IsHostWhitelisted(string host) { return IsHostInList(host, this.WhitelistHosts, this.WhitelistHostsRegex); } /// /// Determines whether a given host is blacklisted. /// /// The host name to test. /// /// true if the host is blacklisted; otherwise, false. /// private bool IsHostBlacklisted(string host) { return IsHostInList(host, this.BlacklistHosts, this.BlacklistHostsRegex); } /// /// Verify that the request qualifies under our security policies /// /// The request URI. /// Thrown when the URI is disallowed for security reasons. private void EnsureAllowableRequestUri(Uri requestUri) { ErrorUtilities.VerifyProtocol( this.IsUriAllowable(requestUri), MessagingStrings.UnsafeWebRequestDetected, requestUri); ErrorUtilities.VerifyProtocol( !this.IsSslRequired || string.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase), MessagingStrings.InsecureWebRequestWithSslRequired, requestUri); } /// /// Determines whether a URI is allowed based on scheme and host name. /// No requireSSL check is done here /// /// The URI to test for whether it should be allowed. /// /// true if [is URI allowable] [the specified URI]; otherwise, false. /// private bool IsUriAllowable(Uri uri) { Requires.NotNull(uri, "uri"); if (!this.allowableSchemes.Contains(uri.Scheme)) { Logger.Http.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri); return false; } // Allow for whitelist or blacklist to override our detection. Func failsUnlessWhitelisted = (string reason) => { if (IsHostWhitelisted(uri.DnsSafeHost)) { return true; } Logger.Http.WarnFormat("Rejecting URL {0} because {1}.", uri, reason); return false; }; // Try to interpret the hostname as an IP address so we can test for internal // IP address ranges. Note that IP addresses can appear in many forms // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1 // So we convert them to a canonical IPAddress instance, and test for all // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1 // Note that Uri.IsLoopback is very unreliable, not catching many of these variants. IPAddress hostIPAddress; if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress)) { byte[] addressBytes = hostIPAddress.GetAddressBytes(); // The host is actually an IP address. switch (hostIPAddress.AddressFamily) { case System.Net.Sockets.AddressFamily.InterNetwork: if (addressBytes[0] == 127 || addressBytes[0] == 10) { return failsUnlessWhitelisted("it is a loopback address."); } break; case System.Net.Sockets.AddressFamily.InterNetworkV6: if (IsIPv6Loopback(hostIPAddress)) { return failsUnlessWhitelisted("it is a loopback address."); } break; default: return failsUnlessWhitelisted("it does not use an IPv4 or IPv6 address."); } } else { // The host is given by name. We require names to contain periods to // help make sure it's not an internal address. if (!uri.Host.Contains(".")) { return failsUnlessWhitelisted("it does not contain a period in the host name."); } } if (this.IsHostBlacklisted(uri.DnsSafeHost)) { Logger.Http.WarnFormat("Rejected URL {0} because it is blacklisted.", uri); return false; } return true; } } }