//-----------------------------------------------------------------------
//
// 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;
}
}
}