PageRenderTime 26ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/DotNetOpenId/UntrustedWebRequest.cs

https://github.com/tt/dotnetopenid
C# | 322 lines | 219 code | 17 blank | 86 comment | 57 complexity | 280d92b81ca943d3a1b86a17b10fc634 MD5 | raw file
  1. #if DEBUG
  2. #define LONGTIMEOUT
  3. #endif
  4. namespace DotNetOpenId {
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.Globalization;
  9. using System.IO;
  10. using System.Net;
  11. using System.Text.RegularExpressions;
  12. using System.Configuration;
  13. using DotNetOpenId.Configuration;
  14. /// <summary>
  15. /// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote
  16. /// server leaving dangling connections, sending too much data, causing requests against
  17. /// internal servers, etc.
  18. /// </summary>
  19. /// <remarks>
  20. /// Protections include:
  21. /// * Conservative maximum time to receive the complete response.
  22. /// * Only HTTP and HTTPS schemes are permitted.
  23. /// * Internal IP address ranges are not permitted: 127.*.*.*, 1::*
  24. /// * Internal host names are not permitted (periods must be found in the host name)
  25. /// If a particular host would be permitted but is in the blacklist, it is not allowed.
  26. /// If a particular host would not be permitted but is in the whitelist, it is allowed.
  27. /// </remarks>
  28. public static class UntrustedWebRequest {
  29. static Configuration.UntrustedWebRequestSection Configuration {
  30. get { return UntrustedWebRequestSection.Configuration; }
  31. }
  32. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  33. static int maximumBytesToRead = Configuration.MaximumBytesToRead;
  34. /// <summary>
  35. /// The default maximum bytes to read in any given HTTP request.
  36. /// Default is 1MB. Cannot be less than 2KB.
  37. /// </summary>
  38. public static int MaximumBytesToRead {
  39. get { return maximumBytesToRead; }
  40. set {
  41. if (value < 2048) throw new ArgumentOutOfRangeException("value");
  42. maximumBytesToRead = value;
  43. }
  44. }
  45. [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  46. static int maximumRedirections = Configuration.MaximumRedirections;
  47. /// <summary>
  48. /// The total number of redirections to allow on any one request.
  49. /// Default is 10.
  50. /// </summary>
  51. public static int MaximumRedirections {
  52. get { return maximumRedirections; }
  53. set {
  54. if (value < 0) throw new ArgumentOutOfRangeException("value");
  55. maximumRedirections = value;
  56. }
  57. }
  58. /// <summary>
  59. /// Gets the time allowed to wait for single read or write operation to complete.
  60. /// Default is 500 milliseconds.
  61. /// </summary>
  62. public static TimeSpan ReadWriteTimeout { get; set; }
  63. /// <summary>
  64. /// Gets the time allowed for an entire HTTP request.
  65. /// Default is 5 seconds.
  66. /// </summary>
  67. public static TimeSpan Timeout { get; set; }
  68. internal delegate UntrustedWebResponse MockRequestResponse(Uri uri, byte[] body, string[] acceptTypes);
  69. /// <summary>
  70. /// Used in unit testing to mock HTTP responses to expected requests.
  71. /// </summary>
  72. /// <remarks>
  73. /// If null, no mocking will take place. But if non-null, all requests
  74. /// will be channeled through this mock method for processing.
  75. /// </remarks>
  76. internal static MockRequestResponse MockRequests;
  77. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
  78. static UntrustedWebRequest() {
  79. ReadWriteTimeout = Configuration.ReadWriteTimeout;
  80. Timeout = Configuration.Timeout;
  81. #if LONGTIMEOUT
  82. ReadWriteTimeout = TimeSpan.FromHours(1);
  83. Timeout = TimeSpan.FromHours(1);
  84. #endif
  85. }
  86. static bool isIPv6Loopback(IPAddress ip) {
  87. Debug.Assert(ip != null);
  88. byte[] addressBytes = ip.GetAddressBytes();
  89. for (int i = 0; i < addressBytes.Length - 1; i++)
  90. if (addressBytes[i] != 0) return false;
  91. if (addressBytes[addressBytes.Length - 1] != 1) return false;
  92. return true;
  93. }
  94. static ICollection<string> allowableSchemes = new List<string> { "http", "https" };
  95. static ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings);
  96. /// <summary>
  97. /// A collection of host name literals that should be allowed even if they don't
  98. /// pass standard security checks.
  99. /// </summary>
  100. public static ICollection<string> WhitelistHosts { get { return whitelistHosts; } }
  101. static ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs);
  102. /// <summary>
  103. /// A collection of host name regular expressions that indicate hosts that should
  104. /// be allowed even though they don't pass standard security checks.
  105. /// </summary>
  106. public static ICollection<Regex> WhitelistHostsRegex { get { return whitelistHostsRegex; } }
  107. static ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings);
  108. /// <summary>
  109. /// A collection of host name literals that should be rejected even if they
  110. /// pass standard security checks.
  111. /// </summary>
  112. public static ICollection<string> BlacklistHosts { get { return blacklistHosts; } }
  113. static ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs);
  114. /// <summary>
  115. /// A collection of host name regular expressions that indicate hosts that should
  116. /// be rjected even if they pass standard security checks.
  117. /// </summary>
  118. public static ICollection<Regex> BlacklistHostsRegex { get { return blacklistHostsRegex; } }
  119. static bool isHostWhitelisted(string host) {
  120. return isHostInList(host, WhitelistHosts, WhitelistHostsRegex);
  121. }
  122. static bool isHostBlacklisted(string host) {
  123. return isHostInList(host, BlacklistHosts, BlacklistHostsRegex);
  124. }
  125. static bool isHostInList(string host, ICollection<string> stringList, ICollection<Regex> regexList) {
  126. Debug.Assert(!string.IsNullOrEmpty(host));
  127. Debug.Assert(stringList != null);
  128. Debug.Assert(regexList != null);
  129. foreach (string testHost in stringList) {
  130. if (string.Equals(host, testHost, StringComparison.OrdinalIgnoreCase))
  131. return true;
  132. }
  133. foreach (Regex regex in regexList) {
  134. if (regex.IsMatch(host))
  135. return true;
  136. }
  137. return false;
  138. }
  139. static bool isUriAllowable(Uri uri) {
  140. Debug.Assert(uri != null);
  141. if (!allowableSchemes.Contains(uri.Scheme)) {
  142. Logger.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
  143. return false;
  144. }
  145. // Allow for whitelist or blacklist to override our detection.
  146. DotNetOpenId.Util.Func<string, bool> failsUnlessWhitelisted = (string reason) => {
  147. if (isHostWhitelisted(uri.DnsSafeHost)) return true;
  148. Logger.WarnFormat("Rejecting URL {0} because {1}.", uri, reason);
  149. return false;
  150. };
  151. // Try to interpret the hostname as an IP address so we can test for internal
  152. // IP address ranges. Note that IP addresses can appear in many forms
  153. // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1
  154. // So we convert them to a canonical IPAddress instance, and test for all
  155. // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1
  156. // Note that Uri.IsLoopback is very unreliable, not catching many of these variants.
  157. IPAddress hostIPAddress;
  158. if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress)) {
  159. byte[] addressBytes = hostIPAddress.GetAddressBytes();
  160. // The host is actually an IP address.
  161. switch (hostIPAddress.AddressFamily) {
  162. case System.Net.Sockets.AddressFamily.InterNetwork:
  163. if (addressBytes[0] == 127 || addressBytes[0] == 10)
  164. return failsUnlessWhitelisted("it is a loopback address.");
  165. break;
  166. case System.Net.Sockets.AddressFamily.InterNetworkV6:
  167. if (isIPv6Loopback(hostIPAddress))
  168. return failsUnlessWhitelisted("it is a loopback address.");
  169. break;
  170. default:
  171. return failsUnlessWhitelisted("it does not use an IPv4 or IPv6 address.");
  172. }
  173. } else {
  174. // The host is given by name. We require names to contain periods to
  175. // help make sure it's not an internal address.
  176. if (!uri.Host.Contains(".")) {
  177. return failsUnlessWhitelisted("it does not contain a period in the host name.");
  178. }
  179. }
  180. if (isHostBlacklisted(uri.DnsSafeHost)) {
  181. Logger.WarnFormat("Rejected URL {0} because it is blacklisted.", uri);
  182. return false;
  183. }
  184. return true;
  185. }
  186. /// <summary>
  187. /// Reads a maximum number of bytes from a response stream.
  188. /// </summary>
  189. /// <returns>
  190. /// The number of bytes actually read.
  191. /// WARNING: This can be fewer than the size of the returned buffer.
  192. /// </returns>
  193. static void readData(HttpWebResponse resp, out byte[] buffer, out int length) {
  194. int bufferSize = resp.ContentLength >= 0 && resp.ContentLength < int.MaxValue ?
  195. Math.Min(MaximumBytesToRead, (int)resp.ContentLength) : MaximumBytesToRead;
  196. buffer = new byte[bufferSize];
  197. using (Stream stream = resp.GetResponseStream()) {
  198. int dataLength = 0;
  199. int chunkSize;
  200. while (dataLength < bufferSize && (chunkSize = stream.Read(buffer, dataLength, bufferSize - dataLength)) > 0)
  201. dataLength += chunkSize;
  202. length = dataLength;
  203. }
  204. }
  205. static UntrustedWebResponse getResponse(Uri requestUri, HttpWebResponse resp) {
  206. byte[] data;
  207. int length;
  208. readData(resp, out data, out length);
  209. return new UntrustedWebResponse(requestUri, resp, new MemoryStream(data, 0, length));
  210. }
  211. internal static UntrustedWebResponse Request(Uri uri) {
  212. return Request(uri, null);
  213. }
  214. internal static UntrustedWebResponse Request(Uri uri, byte[] body) {
  215. return Request(uri, body, null);
  216. }
  217. internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes) {
  218. return Request(uri, body, acceptTypes, false);
  219. }
  220. internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes, bool requireSsl) {
  221. // Since we may require SSL for every redirect, we handle each redirect manually
  222. // in order to detect and fail if any redirect sends us to an HTTP url.
  223. // We COULD allow automatic redirect in the cases where HTTPS is not required,
  224. // but our mock request infrastructure can't do redirects on its own either.
  225. Uri originalRequestUri = uri;
  226. int i;
  227. for (i = 0; i < MaximumRedirections; i++) {
  228. UntrustedWebResponse response = RequestInternal(uri, body, acceptTypes, requireSsl, false, originalRequestUri);
  229. if (response.StatusCode == HttpStatusCode.MovedPermanently ||
  230. response.StatusCode == HttpStatusCode.Redirect ||
  231. response.StatusCode == HttpStatusCode.RedirectMethod ||
  232. response.StatusCode == HttpStatusCode.RedirectKeepVerb) {
  233. uri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]);
  234. } else {
  235. return response;
  236. }
  237. }
  238. throw new WebException(string.Format(CultureInfo.CurrentCulture, Strings.TooManyRedirects, originalRequestUri));
  239. }
  240. static UntrustedWebResponse RequestInternal(Uri uri, byte[] body, string[] acceptTypes,
  241. bool requireSsl, bool avoidSendingExpect100Continue, Uri originalRequestUri) {
  242. if (uri == null) throw new ArgumentNullException("uri");
  243. if (originalRequestUri == null) throw new ArgumentNullException("originalRequestUri");
  244. if (!isUriAllowable(uri)) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
  245. Strings.UnsafeWebRequestDetected, uri), "uri");
  246. if (requireSsl && !String.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
  247. throw new OpenIdException(string.Format(CultureInfo.CurrentCulture, Strings.InsecureWebRequestWithSslRequired, uri));
  248. }
  249. // mock the request if a hosting unit test has configured it.
  250. if (MockRequests != null) {
  251. return MockRequests(uri, body, acceptTypes);
  252. }
  253. HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
  254. // If SSL is required throughout, we cannot allow auto redirects because
  255. // it may include a pass through an unprotected HTTP request.
  256. // We have to follow redirects manually, and our caller will be responsible for that.
  257. request.AllowAutoRedirect = false;
  258. request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
  259. request.Timeout = (int)Timeout.TotalMilliseconds;
  260. request.KeepAlive = false;
  261. if (acceptTypes != null)
  262. request.Accept = string.Join(",", acceptTypes);
  263. if (body != null) {
  264. request.ContentType = "application/x-www-form-urlencoded";
  265. request.ContentLength = body.Length;
  266. request.Method = "POST";
  267. if (avoidSendingExpect100Continue) {
  268. // Some OpenID servers doesn't understand Expect header and send 417 error back.
  269. // If this server just failed from that, we're trying again without sending the
  270. // "Expect: 100-Continue" HTTP header. (see Google Code Issue 72)
  271. // We don't just set Expect100Continue = !avoidSendingExpect100Continue
  272. // so that future requests don't reset this and have to try twice as well.
  273. // We don't want to blindly set all ServicePoints to not use the Expect header
  274. // as that would be a security hole allowing any visitor to a web site change
  275. // the web site's global behavior when calling that host.
  276. request.ServicePoint.Expect100Continue = false;
  277. }
  278. }
  279. try {
  280. if (body != null) {
  281. using (Stream outStream = request.GetRequestStream()) {
  282. outStream.Write(body, 0, body.Length);
  283. }
  284. }
  285. using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
  286. return getResponse(originalRequestUri, response);
  287. }
  288. } catch (WebException e) {
  289. using (HttpWebResponse response = (HttpWebResponse)e.Response) {
  290. if (response != null) {
  291. if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
  292. if (!avoidSendingExpect100Continue) { // must only try this once more
  293. return RequestInternal(uri, body, acceptTypes, requireSsl, true, originalRequestUri);
  294. }
  295. }
  296. return getResponse(originalRequestUri, response);
  297. } else {
  298. throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
  299. Strings.WebRequestFailed, originalRequestUri), e);
  300. }
  301. }
  302. }
  303. }
  304. }
  305. }