UntrustedWebRequest.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.IO;
  30. using System.Net;
  31. using System.Net.Security;
  32. using System.Text;
  33. using log4net;
  34. namespace OpenSim.Framework
  35. {
  36. /// <summary>
  37. /// Used for requests to untrusted endpoints that may potentially be
  38. /// malicious
  39. /// </summary>
  40. public static class UntrustedHttpWebRequest
  41. {
  42. /// <summary>Setting this to true will allow HTTP connections to localhost</summary>
  43. private const bool DEBUG = true;
  44. private static readonly ILog m_log =
  45. LogManager.GetLogger(
  46. System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
  47. private static readonly ICollection<string> allowableSchemes = new List<string> { "http", "https" };
  48. /// <summary>
  49. /// Creates an HttpWebRequest that is hardened against malicious
  50. /// endpoints after ensuring the given Uri is safe to retrieve
  51. /// </summary>
  52. /// <param name="uri">Web location to request</param>
  53. /// <returns>A hardened HttpWebRequest if the uri was determined to be safe</returns>
  54. /// <exception cref="ArgumentNullException">If uri is null</exception>
  55. /// <exception cref="ArgumentException">If uri is unsafe</exception>
  56. public static HttpWebRequest Create(Uri uri)
  57. {
  58. return Create(uri, DEBUG, 1000 * 5, 1000 * 20, 10);
  59. }
  60. /// <summary>
  61. /// Creates an HttpWebRequest that is hardened against malicious
  62. /// endpoints after ensuring the given Uri is safe to retrieve
  63. /// </summary>
  64. /// <param name="uri">Web location to request</param>
  65. /// <param name="allowLoopback">True to allow connections to localhost, otherwise false</param>
  66. /// <param name="readWriteTimeoutMS">Read write timeout, in milliseconds</param>
  67. /// <param name="timeoutMS">Connection timeout, in milliseconds</param>
  68. /// <param name="maximumRedirects">Maximum number of allowed redirects</param>
  69. /// <returns>A hardened HttpWebRequest if the uri was determined to be safe</returns>
  70. /// <exception cref="ArgumentNullException">If uri is null</exception>
  71. /// <exception cref="ArgumentException">If uri is unsafe</exception>
  72. public static HttpWebRequest Create(Uri uri, bool allowLoopback, int readWriteTimeoutMS, int timeoutMS, int maximumRedirects)
  73. {
  74. if (uri == null)
  75. throw new ArgumentNullException("uri");
  76. if (!IsUriAllowable(uri, allowLoopback))
  77. throw new ArgumentException("Uri " + uri + " was rejected");
  78. HttpWebRequest httpWebRequest = (HttpWebRequest)HttpWebRequest.Create(uri);
  79. httpWebRequest.MaximumAutomaticRedirections = maximumRedirects;
  80. httpWebRequest.ReadWriteTimeout = readWriteTimeoutMS;
  81. httpWebRequest.Timeout = timeoutMS;
  82. httpWebRequest.KeepAlive = false;
  83. return httpWebRequest;
  84. }
  85. public static string PostToUntrustedUrl(Uri url, string data)
  86. {
  87. try
  88. {
  89. byte[] requestData = System.Text.Encoding.UTF8.GetBytes(data);
  90. HttpWebRequest request = Create(url);
  91. request.Method = "POST";
  92. request.ContentLength = requestData.Length;
  93. request.ContentType = "application/x-www-form-urlencoded";
  94. using (Stream requestStream = request.GetRequestStream())
  95. requestStream.Write(requestData, 0, requestData.Length);
  96. using (WebResponse response = request.GetResponse())
  97. {
  98. using (Stream responseStream = response.GetResponseStream())
  99. return responseStream.GetStreamString();
  100. }
  101. }
  102. catch (Exception ex)
  103. {
  104. m_log.Warn("POST to untrusted URL " + url + " failed: " + ex.Message);
  105. return null;
  106. }
  107. }
  108. public static string GetUntrustedUrl(Uri url)
  109. {
  110. try
  111. {
  112. HttpWebRequest request = Create(url);
  113. using (WebResponse response = request.GetResponse())
  114. {
  115. using (Stream responseStream = response.GetResponseStream())
  116. return responseStream.GetStreamString();
  117. }
  118. }
  119. catch (Exception ex)
  120. {
  121. m_log.Warn("GET from untrusted URL " + url + " failed: " + ex.Message);
  122. return null;
  123. }
  124. }
  125. /// <summary>
  126. /// Determines whether a URI is allowed based on scheme and host name.
  127. /// No requireSSL check is done here
  128. /// </summary>
  129. /// <param name="allowLoopback">True to allow loopback addresses to be used</param>
  130. /// <param name="uri">The URI to test for whether it should be allowed.</param>
  131. /// <returns>
  132. /// <c>true</c> if [is URI allowable] [the specified URI]; otherwise, <c>false</c>.
  133. /// </returns>
  134. private static bool IsUriAllowable(Uri uri, bool allowLoopback)
  135. {
  136. if (!allowableSchemes.Contains(uri.Scheme))
  137. {
  138. m_log.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
  139. return false;
  140. }
  141. // Try to interpret the hostname as an IP address so we can test for internal
  142. // IP address ranges. Note that IP addresses can appear in many forms
  143. // (e.g. http://127.0.0.1, http://2130706433, http://0x0100007f, http://::1
  144. // So we convert them to a canonical IPAddress instance, and test for all
  145. // non-routable IP ranges: 10.*.*.*, 127.*.*.*, ::1
  146. // Note that Uri.IsLoopback is very unreliable, not catching many of these variants.
  147. IPAddress hostIPAddress;
  148. if (IPAddress.TryParse(uri.DnsSafeHost, out hostIPAddress))
  149. {
  150. byte[] addressBytes = hostIPAddress.GetAddressBytes();
  151. // The host is actually an IP address.
  152. switch (hostIPAddress.AddressFamily)
  153. {
  154. case System.Net.Sockets.AddressFamily.InterNetwork:
  155. if (!allowLoopback && (addressBytes[0] == 127 || addressBytes[0] == 10))
  156. {
  157. m_log.WarnFormat("Rejecting URL {0} because it is a loopback address.", uri);
  158. return false;
  159. }
  160. break;
  161. case System.Net.Sockets.AddressFamily.InterNetworkV6:
  162. if (!allowLoopback && IsIPv6Loopback(hostIPAddress))
  163. {
  164. m_log.WarnFormat("Rejecting URL {0} because it is a loopback address.", uri);
  165. return false;
  166. }
  167. break;
  168. default:
  169. m_log.WarnFormat("Rejecting URL {0} because it does not use an IPv4 or IPv6 address.", uri);
  170. return false;
  171. }
  172. }
  173. else
  174. {
  175. // The host is given by name. We require names to contain periods to
  176. // help make sure it's not an internal address.
  177. if (!allowLoopback && !uri.Host.Contains("."))
  178. {
  179. m_log.WarnFormat("Rejecting URL {0} because it does not contain a period in the host name.", uri);
  180. return false;
  181. }
  182. }
  183. return true;
  184. }
  185. /// <summary>
  186. /// Determines whether an IP address is the IPv6 equivalent of "localhost/127.0.0.1".
  187. /// </summary>
  188. /// <param name="ip">The ip address to check.</param>
  189. /// <returns>
  190. /// <c>true</c> if this is a loopback IP address; <c>false</c> otherwise.
  191. /// </returns>
  192. private static bool IsIPv6Loopback(IPAddress ip)
  193. {
  194. if (ip == null)
  195. throw new ArgumentNullException("ip");
  196. byte[] addressBytes = ip.GetAddressBytes();
  197. for (int i = 0; i < addressBytes.Length - 1; i++)
  198. {
  199. if (addressBytes[i] != 0)
  200. return false;
  201. }
  202. if (addressBytes[addressBytes.Length - 1] != 1)
  203. return false;
  204. return true;
  205. }
  206. }
  207. }