summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs')
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs477
1 files changed, 0 insertions, 477 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs b/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs
deleted file mode 100644
index be0182d..0000000
--- a/src/DotNetOpenAuth.Core/Messaging/UntrustedWebRequestHandler.cs
+++ /dev/null
@@ -1,477 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="UntrustedWebRequestHandler.cs" company="Outercurve Foundation">
-// Copyright (c) Outercurve Foundation. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.Messaging {
- 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.Text.RegularExpressions;
- using DotNetOpenAuth.Configuration;
- using DotNetOpenAuth.Messaging;
- using Validation;
-
- /// <summary>
- /// 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.
- /// </summary>
- /// <remarks>
- /// 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.
- /// </remarks>
- public class UntrustedWebRequestHandler : IDirectWebRequestHandler {
- /// <summary>
- /// The set of URI schemes allowed in untrusted web requests.
- /// </summary>
- private ICollection<string> allowableSchemes = new List<string> { "http", "https" };
-
- /// <summary>
- /// The collection of blacklisted hosts.
- /// </summary>
- private ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings);
-
- /// <summary>
- /// The collection of regular expressions used to identify additional blacklisted hosts.
- /// </summary>
- private ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs);
-
- /// <summary>
- /// The collection of whitelisted hosts.
- /// </summary>
- private ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings);
-
- /// <summary>
- /// The collection of regular expressions used to identify additional whitelisted hosts.
- /// </summary>
- private ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs);
-
- /// <summary>
- /// The maximum redirections to follow in the course of a single request.
- /// </summary>
- [DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private int maximumRedirections = Configuration.MaximumRedirections;
-
- /// <summary>
- /// The maximum number of bytes to read from the response of an untrusted server.
- /// </summary>
- [DebuggerBrowsable(DebuggerBrowsableState.Never)]
- private int maximumBytesToRead = Configuration.MaximumBytesToRead;
-
- /// <summary>
- /// The handler that will actually send the HTTP request and collect
- /// the response once the untrusted server gates have been satisfied.
- /// </summary>
- private IDirectWebRequestHandler chainedWebRequestHandler;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class.
- /// </summary>
- public UntrustedWebRequestHandler()
- : this(new StandardWebRequestHandler()) {
- }
-
- /// <summary>
- /// Initializes a new instance of the <see cref="UntrustedWebRequestHandler"/> class.
- /// </summary>
- /// <param name="chainedWebRequestHandler">The chained web request handler.</param>
- public UntrustedWebRequestHandler(IDirectWebRequestHandler chainedWebRequestHandler) {
- Requires.NotNull(chainedWebRequestHandler, "chainedWebRequestHandler");
-
- this.chainedWebRequestHandler = chainedWebRequestHandler;
- if (Debugger.IsAttached) {
- // Since a debugger is attached, requests may be MUCH slower,
- // so give ourselves huge timeouts.
- this.ReadWriteTimeout = TimeSpan.FromHours(1);
- this.Timeout = TimeSpan.FromHours(1);
- } else {
- this.ReadWriteTimeout = Configuration.ReadWriteTimeout;
- this.Timeout = Configuration.Timeout;
- }
- }
-
- /// <summary>
- /// Gets or sets the default maximum bytes to read in any given HTTP request.
- /// </summary>
- /// <value>Default is 1MB. Cannot be less than 2KB.</value>
- public int MaximumBytesToRead {
- get {
- return this.maximumBytesToRead;
- }
-
- set {
- Requires.Range(value >= 2048, "value");
- this.maximumBytesToRead = value;
- }
- }
-
- /// <summary>
- /// Gets or sets the total number of redirections to allow on any one request.
- /// Default is 10.
- /// </summary>
- public int MaximumRedirections {
- get {
- return this.maximumRedirections;
- }
-
- set {
- Requires.Range(value >= 0, "value");
- this.maximumRedirections = value;
- }
- }
-
- /// <summary>
- /// Gets or sets the time allowed to wait for single read or write operation to complete.
- /// Default is 500 milliseconds.
- /// </summary>
- public TimeSpan ReadWriteTimeout { get; set; }
-
- /// <summary>
- /// Gets or sets the time allowed for an entire HTTP request.
- /// Default is 5 seconds.
- /// </summary>
- public TimeSpan Timeout { get; set; }
-
- /// <summary>
- /// Gets a collection of host name literals that should be allowed even if they don't
- /// pass standard security checks.
- /// </summary>
- [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")]
- public ICollection<string> WhitelistHosts { get { return this.whitelistHosts; } }
-
- /// <summary>
- /// Gets a collection of host name regular expressions that indicate hosts that should
- /// be allowed even though they don't pass standard security checks.
- /// </summary>
- [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Justification = "Spelling as intended.")]
- public ICollection<Regex> WhitelistHostsRegex { get { return this.whitelistHostsRegex; } }
-
- /// <summary>
- /// Gets a collection of host name literals that should be rejected even if they
- /// pass standard security checks.
- /// </summary>
- public ICollection<string> BlacklistHosts { get { return this.blacklistHosts; } }
-
- /// <summary>
- /// Gets a collection of host name regular expressions that indicate hosts that should
- /// be rejected even if they pass standard security checks.
- /// </summary>
- public ICollection<Regex> BlacklistHostsRegex { get { return this.blacklistHostsRegex; } }
-
- /// <summary>
- /// Gets the configuration for this class that is specified in the host's .config file.
- /// </summary>
- private static UntrustedWebRequestElement Configuration {
- get { return DotNetOpenAuthSection.Messaging.UntrustedWebRequest; }
- }
-
- #region IDirectWebRequestHandler Members
-
- /// <summary>
- /// Determines whether this instance can support the specified options.
- /// </summary>
- /// <param name="options">The set of options that might be given in a subsequent web request.</param>
- /// <returns>
- /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>.
- /// </returns>
- [Pure]
- public bool CanSupport(DirectWebRequestOptions options) {
- // We support whatever our chained handler supports, plus RequireSsl.
- return this.chainedWebRequestHandler.CanSupport(options & ~DirectWebRequestOptions.RequireSsl);
- }
-
- /// <summary>
- /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
- /// </summary>
- /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
- /// <param name="options">The options to apply to this web request.</param>
- /// <returns>
- /// The writer the caller should write out the entity data to.
- /// </returns>
- /// <exception cref="ProtocolException">Thrown for any network error.</exception>
- /// <remarks>
- /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/>
- /// and any other appropriate properties <i>before</i> calling this method.</para>
- /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
- /// <see cref="ProtocolException"/> to abstract away the transport and provide
- /// a single exception type for hosts to catch.</para>
- /// </remarks>
- public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) {
- this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0);
-
- this.PrepareRequest(request, true);
-
- // Submit the request and get the request stream back.
- return this.chainedWebRequestHandler.GetRequestStream(request, options & ~DirectWebRequestOptions.RequireSsl);
- }
-
- /// <summary>
- /// Processes an <see cref="HttpWebRequest"/> and converts the
- /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
- /// </summary>
- /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
- /// <param name="options">The options to apply to this web request.</param>
- /// <returns>
- /// An instance of <see cref="CachedDirectWebResponse"/> describing the response.
- /// </returns>
- /// <exception cref="ProtocolException">Thrown for any network error.</exception>
- /// <remarks>
- /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
- /// <see cref="ProtocolException"/> to abstract away the transport and provide
- /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
- /// value, if set, should be Closed before throwing.</para>
- /// </remarks>
- [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")]
- public IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options) {
- // This request MAY have already been prepared by GetRequestStream, but
- // we have no guarantee, so do it just to be safe.
- this.PrepareRequest(request, false);
-
- // 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.MaximumRedirections; i++) {
- this.EnsureAllowableRequestUri(request.RequestUri, (options & DirectWebRequestOptions.RequireSsl) != 0);
- CachedDirectWebResponse response = this.chainedWebRequestHandler.GetResponse(request, options & ~DirectWebRequestOptions.RequireSsl).GetSnapshot(this.MaximumBytesToRead);
- if (response.Status == HttpStatusCode.MovedPermanently ||
- response.Status == HttpStatusCode.Redirect ||
- response.Status == HttpStatusCode.RedirectMethod ||
- response.Status == 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 != "POST", MessagingStrings.UntrustedRedirectsOnPOSTNotSupported);
- Uri redirectUri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]);
- request = request.Clone(redirectUri);
- } else {
- if (response.FinalUri != request.RequestUri) {
- // Since we don't automatically follow redirects, there's only one scenario where this
- // can happen: when the server sends a (non-redirecting) Content-Location header in the response.
- // It's imperative that we do not trust that header though, so coerce the FinalUri to be
- // what we just requested.
- Logger.Http.WarnFormat("The response from {0} included an HTTP header indicating it's the same as {1}, but it's not a redirect so we won't trust that.", request.RequestUri, response.FinalUri);
- response.FinalUri = request.RequestUri;
- }
-
- return response;
- }
- }
-
- throw ErrorUtilities.ThrowProtocol(MessagingStrings.TooManyRedirects, originalRequestUri);
- }
-
- /// <summary>
- /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity.
- /// </summary>
- /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param>
- /// <returns>
- /// The writer the caller should write out the entity data to.
- /// </returns>
- Stream IDirectWebRequestHandler.GetRequestStream(HttpWebRequest request) {
- return this.GetRequestStream(request, DirectWebRequestOptions.None);
- }
-
- /// <summary>
- /// Processes an <see cref="HttpWebRequest"/> and converts the
- /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance.
- /// </summary>
- /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param>
- /// <returns>
- /// An instance of <see cref="IncomingWebResponse"/> describing the response.
- /// </returns>
- /// <exception cref="ProtocolException">Thrown for any network error.</exception>
- /// <remarks>
- /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a
- /// <see cref="ProtocolException"/> to abstract away the transport and provide
- /// a single exception type for hosts to catch. The <see cref="WebException.Response"/>
- /// value, if set, should be Closed before throwing.</para>
- /// </remarks>
- IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) {
- return this.GetResponse(request, DirectWebRequestOptions.None);
- }
-
- #endregion
-
- /// <summary>
- /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1".
- /// </summary>
- /// <param name="ip">The ip address to check.</param>
- /// <returns>
- /// <c>true</c> if this is a loopback IP address; <c>false</c> otherwise.
- /// </returns>
- 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;
- }
-
- /// <summary>
- /// Determines whether the given host name is in a host list or host name regex list.
- /// </summary>
- /// <param name="host">The host name.</param>
- /// <param name="stringList">The list of host names.</param>
- /// <param name="regexList">The list of regex patterns of host names.</param>
- /// <returns>
- /// <c>true</c> if the specified host falls within at least one of the given lists; otherwise, <c>false</c>.
- /// </returns>
- private static bool IsHostInList(string host, ICollection<string> stringList, ICollection<Regex> 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;
- }
-
- /// <summary>
- /// Determines whether a given host is whitelisted.
- /// </summary>
- /// <param name="host">The host name to test.</param>
- /// <returns>
- /// <c>true</c> if the host is whitelisted; otherwise, <c>false</c>.
- /// </returns>
- private bool IsHostWhitelisted(string host) {
- return IsHostInList(host, this.WhitelistHosts, this.WhitelistHostsRegex);
- }
-
- /// <summary>
- /// Determines whether a given host is blacklisted.
- /// </summary>
- /// <param name="host">The host name to test.</param>
- /// <returns>
- /// <c>true</c> if the host is blacklisted; otherwise, <c>false</c>.
- /// </returns>
- private bool IsHostBlacklisted(string host) {
- return IsHostInList(host, this.BlacklistHosts, this.BlacklistHostsRegex);
- }
-
- /// <summary>
- /// Verify that the request qualifies under our security policies
- /// </summary>
- /// <param name="requestUri">The request URI.</param>
- /// <param name="requireSsl">If set to <c>true</c>, only web requests that can be made entirely over SSL will succeed.</param>
- /// <exception cref="ProtocolException">Thrown when the URI is disallowed for security reasons.</exception>
- private void EnsureAllowableRequestUri(Uri requestUri, bool requireSsl) {
- ErrorUtilities.VerifyProtocol(this.IsUriAllowable(requestUri), MessagingStrings.UnsafeWebRequestDetected, requestUri);
- ErrorUtilities.VerifyProtocol(!requireSsl || string.Equals(requestUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase), MessagingStrings.InsecureWebRequestWithSslRequired, requestUri);
- }
-
- /// <summary>
- /// Determines whether a URI is allowed based on scheme and host name.
- /// No requireSSL check is done here
- /// </summary>
- /// <param name="uri">The URI to test for whether it should be allowed.</param>
- /// <returns>
- /// <c>true</c> if [is URI allowable] [the specified URI]; otherwise, <c>false</c>.
- /// </returns>
- 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<string, bool> 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;
- }
-
- /// <summary>
- /// Prepares the request by setting timeout and redirect policies.
- /// </summary>
- /// <param name="request">The request to prepare.</param>
- /// <param name="preparingPost"><c>true</c> if this is a POST request whose headers have not yet been sent out; <c>false</c> otherwise.</param>
- private void PrepareRequest(HttpWebRequest request, bool preparingPost) {
- Requires.NotNull(request, "request");
-
- // Be careful to not try to change the HTTP headers that have already gone out.
- if (preparingPost || request.Method == "GET") {
- // Set/override a few properties of the request to apply our policies for untrusted requests.
- request.ReadWriteTimeout = (int)this.ReadWriteTimeout.TotalMilliseconds;
- request.Timeout = (int)this.Timeout.TotalMilliseconds;
- request.KeepAlive = false;
- }
-
- // 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.
- request.AllowAutoRedirect = false;
- }
- }
-}