BasicMiddleware 269 B

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. commit 8b58a9a0917315cef4aac6d0ffb44144d9df9bf1
  2. Author: Chris Ross (ASP.NET) <[email protected]>
  3. Date: Fri Jan 12 12:58:56 2018 -0800
  4. Scheme and Host value validation
  5. diff --git a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
  6. index 1863087c0ee..d5e074de57d 100644
  7. --- a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
  8. +++ b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
  9. @@ -2,25 +2,63 @@
  10. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  11. using System;
  12. -using System.Collections.Generic;
  13. using System.Linq;
  14. using System.Net;
  15. +using System.Runtime.CompilerServices;
  16. using System.Threading.Tasks;
  17. using Microsoft.AspNetCore.Builder;
  18. using Microsoft.AspNetCore.Http;
  19. using Microsoft.AspNetCore.HttpOverrides.Internal;
  20. using Microsoft.Extensions.Logging;
  21. using Microsoft.Extensions.Options;
  22. -using Microsoft.Extensions.Primitives;
  23. namespace Microsoft.AspNetCore.HttpOverrides
  24. {
  25. public class ForwardedHeadersMiddleware
  26. {
  27. + private static readonly bool[] HostCharValidity = new bool[127];
  28. + private static readonly bool[] SchemeCharValidity = new bool[123];
  29. +
  30. private readonly ForwardedHeadersOptions _options;
  31. private readonly RequestDelegate _next;
  32. private readonly ILogger _logger;
  33. + static ForwardedHeadersMiddleware()
  34. + {
  35. + // RFC 3986 scheme = ALPHA * (ALPHA / DIGIT / "+" / "-" / ".")
  36. + SchemeCharValidity['+'] = true;
  37. + SchemeCharValidity['-'] = true;
  38. + SchemeCharValidity['.'] = true;
  39. +
  40. + // Host Matches Http.Sys and Kestrel
  41. + // Host Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
  42. + HostCharValidity['!'] = true;
  43. + HostCharValidity['$'] = true;
  44. + HostCharValidity['&'] = true;
  45. + HostCharValidity['\''] = true;
  46. + HostCharValidity['('] = true;
  47. + HostCharValidity[')'] = true;
  48. + HostCharValidity['-'] = true;
  49. + HostCharValidity['.'] = true;
  50. + HostCharValidity['_'] = true;
  51. + HostCharValidity['~'] = true;
  52. + for (var ch = '0'; ch <= '9'; ch++)
  53. + {
  54. + SchemeCharValidity[ch] = true;
  55. + HostCharValidity[ch] = true;
  56. + }
  57. + for (var ch = 'A'; ch <= 'Z'; ch++)
  58. + {
  59. + SchemeCharValidity[ch] = true;
  60. + HostCharValidity[ch] = true;
  61. + }
  62. + for (var ch = 'a'; ch <= 'z'; ch++)
  63. + {
  64. + SchemeCharValidity[ch] = true;
  65. + HostCharValidity[ch] = true;
  66. + }
  67. + }
  68. +
  69. public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ForwardedHeadersOptions> options)
  70. {
  71. if (next == null)
  72. @@ -179,7 +217,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
  73. if (checkProto)
  74. {
  75. - if (!string.IsNullOrEmpty(set.Scheme))
  76. + if (!string.IsNullOrEmpty(set.Scheme) && TryValidateScheme(set.Scheme))
  77. {
  78. applyChanges = true;
  79. currentValues.Scheme = set.Scheme;
  80. @@ -193,7 +231,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
  81. if (checkHost)
  82. {
  83. - if (!string.IsNullOrEmpty(set.Host))
  84. + if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host))
  85. {
  86. applyChanges = true;
  87. currentValues.Host = set.Host;
  88. @@ -288,5 +326,124 @@ namespace Microsoft.AspNetCore.HttpOverrides
  89. public string Host;
  90. public string Scheme;
  91. }
  92. +
  93. + // Empty was checked for by the caller
  94. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  95. + private bool TryValidateScheme(string scheme)
  96. + {
  97. + for (var i = 0; i < scheme.Length; i++)
  98. + {
  99. + if (!IsValidSchemeChar(scheme[i]))
  100. + {
  101. + return false;
  102. + }
  103. + }
  104. + return true;
  105. + }
  106. +
  107. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  108. + private static bool IsValidSchemeChar(char ch)
  109. + {
  110. + return ch < SchemeCharValidity.Length && SchemeCharValidity[ch];
  111. + }
  112. +
  113. + // Empty was checked for by the caller
  114. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  115. + private bool TryValidateHost(string host)
  116. + {
  117. + if (host[0] == '[')
  118. + {
  119. + return TryValidateIPv6Host(host);
  120. + }
  121. +
  122. + if (host[0] == ':')
  123. + {
  124. + // Only a port
  125. + return false;
  126. + }
  127. +
  128. + var i = 0;
  129. + for (; i < host.Length; i++)
  130. + {
  131. + if (!IsValidHostChar(host[i]))
  132. + {
  133. + break;
  134. + }
  135. + }
  136. + return TryValidateHostPort(host, i);
  137. + }
  138. +
  139. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  140. + private static bool IsValidHostChar(char ch)
  141. + {
  142. + return ch < HostCharValidity.Length && HostCharValidity[ch];
  143. + }
  144. +
  145. + // The lead '[' was already checked
  146. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  147. + private bool TryValidateIPv6Host(string hostText)
  148. + {
  149. + for (var i = 1; i < hostText.Length; i++)
  150. + {
  151. + var ch = hostText[i];
  152. + if (ch == ']')
  153. + {
  154. + // [::1] is the shortest valid IPv6 host
  155. + if (i < 4)
  156. + {
  157. + return false;
  158. + }
  159. + return TryValidateHostPort(hostText, i + 1);
  160. + }
  161. +
  162. + if (!IsHex(ch) && ch != ':' && ch != '.')
  163. + {
  164. + return false;
  165. + }
  166. + }
  167. +
  168. + // Must contain a ']'
  169. + return false;
  170. + }
  171. +
  172. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  173. + private bool TryValidateHostPort(string hostText, int offset)
  174. + {
  175. + if (offset == hostText.Length)
  176. + {
  177. + // No port
  178. + return true;
  179. + }
  180. +
  181. + if (hostText[offset] != ':' || hostText.Length == offset + 1)
  182. + {
  183. + // Must have at least one number after the colon if present.
  184. + return false;
  185. + }
  186. +
  187. + for (var i = offset + 1; i < hostText.Length; i++)
  188. + {
  189. + if (!IsNumeric(hostText[i]))
  190. + {
  191. + return false;
  192. + }
  193. + }
  194. +
  195. + return true;
  196. + }
  197. +
  198. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  199. + private bool IsNumeric(char ch)
  200. + {
  201. + return '0' <= ch && ch <= '9';
  202. + }
  203. +
  204. + [MethodImpl(MethodImplOptions.AggressiveInlining)]
  205. + private bool IsHex(char ch)
  206. + {
  207. + return IsNumeric(ch)
  208. + || ('a' <= ch && ch <= 'f')
  209. + || ('A' <= ch && ch <= 'F');
  210. + }
  211. }
  212. }
  213. diff --git a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
  214. index ea905dace0f..81d6ccf15a0 100644
  215. --- a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
  216. +++ b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
  217. @@ -252,6 +252,146 @@ namespace Microsoft.AspNetCore.HttpOverrides
  218. Assert.Equal("testhost", context.Request.Host.ToString());
  219. }
  220. + public static TheoryData<string> HostHeaderData
  221. + {
  222. + get
  223. + {
  224. + return new TheoryData<string>() {
  225. + "z",
  226. + "1",
  227. + "y:1",
  228. + "1:1",
  229. + "[ABCdef]",
  230. + "[abcDEF]:0",
  231. + "[abcdef:127.2355.1246.114]:0",
  232. + "[::1]:80",
  233. + "127.0.0.1:80",
  234. + "900.900.900.900:9523547852",
  235. + "foo",
  236. + "foo:234",
  237. + "foo.bar.baz",
  238. + "foo.BAR.baz:46245",
  239. + "foo.ba-ar.baz:46245",
  240. + "-foo:1234",
  241. + "xn--c1yn36f:134",
  242. + "-",
  243. + "_",
  244. + "~",
  245. + "!",
  246. + "$",
  247. + "'",
  248. + "(",
  249. + ")",
  250. + };
  251. + }
  252. + }
  253. +
  254. + [Theory]
  255. + [MemberData(nameof(HostHeaderData))]
  256. + public async Task XForwardedHostAllowsValidCharacters(string host)
  257. + {
  258. + var assertsExecuted = false;
  259. +
  260. + var builder = new WebHostBuilder()
  261. + .Configure(app =>
  262. + {
  263. + app.UseForwardedHeaders(new ForwardedHeadersOptions
  264. + {
  265. + ForwardedHeaders = ForwardedHeaders.XForwardedHost
  266. + });
  267. + app.Run(context =>
  268. + {
  269. + Assert.Equal(host, context.Request.Host.ToString());
  270. + assertsExecuted = true;
  271. + return Task.FromResult(0);
  272. + });
  273. + });
  274. + var server = new TestServer(builder);
  275. +
  276. + await server.SendAsync(c =>
  277. + {
  278. + c.Request.Headers["X-Forwarded-Host"] = host;
  279. + });
  280. + Assert.True(assertsExecuted);
  281. + }
  282. +
  283. + public static TheoryData<string> HostHeaderInvalidData
  284. + {
  285. + get
  286. + {
  287. + // see https://tools.ietf.org/html/rfc7230#section-5.4
  288. + var data = new TheoryData<string>() {
  289. + "", // Empty
  290. + "[]", // Too short
  291. + "[::]", // Too short
  292. + "[ghijkl]", // Non-hex
  293. + "[afd:adf:123", // Incomplete
  294. + "[afd:adf]123", // Missing :
  295. + "[afd:adf]:", // Missing port digits
  296. + "[afd adf]", // Space
  297. + "[ad-314]", // dash
  298. + ":1234", // Missing host
  299. + "a:b:c", // Missing []
  300. + "::1", // Missing []
  301. + "::", // Missing everything
  302. + "abcd:1abcd", // Letters in port
  303. + "abcd:1.2", // Dot in port
  304. + "1.2.3.4:", // Missing port digits
  305. + "1.2 .4", // Space
  306. + };
  307. +
  308. + // These aren't allowed anywhere in the host header
  309. + var invalid = "\"#%*+/;<=>?@[]\\^`{}|";
  310. + foreach (var ch in invalid)
  311. + {
  312. + data.Add(ch.ToString());
  313. + }
  314. +
  315. + invalid = "!\"#$%&'()*+,/;<=>?@[]\\^_`{}|~-";
  316. + foreach (var ch in invalid)
  317. + {
  318. + data.Add("[abd" + ch + "]:1234");
  319. + }
  320. +
  321. + invalid = "!\"#$%&'()*+/;<=>?@[]\\^_`{}|~:abcABC-.";
  322. + foreach (var ch in invalid)
  323. + {
  324. + data.Add("a.b.c:" + ch);
  325. + }
  326. +
  327. + return data;
  328. + }
  329. + }
  330. +
  331. + [Theory]
  332. + [MemberData(nameof(HostHeaderInvalidData))]
  333. + public async Task XForwardedHostFailsForInvalidCharacters(string host)
  334. + {
  335. + var assertsExecuted = false;
  336. +
  337. + var builder = new WebHostBuilder()
  338. + .Configure(app =>
  339. + {
  340. + app.UseForwardedHeaders(new ForwardedHeadersOptions
  341. + {
  342. + ForwardedHeaders = ForwardedHeaders.XForwardedHost
  343. + });
  344. + app.Run(context =>
  345. + {
  346. + Assert.NotEqual(host, context.Request.Host.Value);
  347. + assertsExecuted = true;
  348. + return Task.FromResult(0);
  349. + });
  350. + });
  351. + var server = new TestServer(builder);
  352. +
  353. + await server.SendAsync(c =>
  354. + {
  355. + c.Request.Headers["X-Forwarded-Host"] = host;
  356. + });
  357. + Assert.True(assertsExecuted);
  358. + }
  359. +
  360. [Theory]
  361. [InlineData(0, "h1", "http")]
  362. [InlineData(1, "", "http")]
  363. @@ -281,6 +421,100 @@ namespace Microsoft.AspNetCore.HttpOverrides
  364. Assert.Equal(expected, context.Request.Scheme);
  365. }
  366. + public static TheoryData<string> ProtoHeaderData
  367. + {
  368. + get
  369. + {
  370. + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
  371. + return new TheoryData<string>() {
  372. + "z",
  373. + "Z",
  374. + "1",
  375. + "y+",
  376. + "1-",
  377. + "a.",
  378. + };
  379. + }
  380. + }
  381. +
  382. + [Theory]
  383. + [MemberData(nameof(ProtoHeaderData))]
  384. + public async Task XForwardedProtoAcceptsValidProtocols(string scheme)
  385. + {
  386. + var assertsExecuted = false;
  387. +
  388. + var builder = new WebHostBuilder()
  389. + .Configure(app =>
  390. + {
  391. + app.UseForwardedHeaders(new ForwardedHeadersOptions
  392. + {
  393. + ForwardedHeaders = ForwardedHeaders.XForwardedProto
  394. + });
  395. + app.Run(context =>
  396. + {
  397. + Assert.Equal(scheme, context.Request.Scheme);
  398. + assertsExecuted = true;
  399. + return Task.FromResult(0);
  400. + });
  401. + });
  402. + var server = new TestServer(builder);
  403. +
  404. + await server.SendAsync(c =>
  405. + {
  406. + c.Request.Headers["X-Forwarded-Proto"] = scheme;
  407. + });
  408. + Assert.True(assertsExecuted);
  409. + }
  410. +
  411. + public static TheoryData<string> ProtoHeaderInvalidData
  412. + {
  413. + get
  414. + {
  415. + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
  416. + var data = new TheoryData<string>() {
  417. + "a b", // Space
  418. + };
  419. +
  420. + // These aren't allowed anywhere in the scheme header
  421. + var invalid = "!\"#$%&'()*/:;<=>?@[]\\^_`{}|~";
  422. + foreach (var ch in invalid)
  423. + {
  424. + data.Add(ch.ToString());
  425. + }
  426. +
  427. + return data;
  428. + }
  429. + }
  430. +
  431. + [Theory]
  432. + [MemberData(nameof(ProtoHeaderInvalidData))]
  433. + public async Task XForwardedProtoRejectsInvalidProtocols(string scheme)
  434. + {
  435. + var assertsExecuted = false;
  436. +
  437. + var builder = new WebHostBuilder()
  438. + .Configure(app =>
  439. + {
  440. + app.UseForwardedHeaders(new ForwardedHeadersOptions
  441. + {
  442. + ForwardedHeaders = ForwardedHeaders.XForwardedProto,
  443. + });
  444. + app.Run(context =>
  445. + {
  446. + Assert.Equal("http", context.Request.Scheme);
  447. + assertsExecuted = true;
  448. + return Task.FromResult(0);
  449. + });
  450. + });
  451. + var server = new TestServer(builder);
  452. +
  453. + await server.SendAsync(c =>
  454. + {
  455. + c.Request.Headers["X-Forwarded-Proto"] = scheme;
  456. + });
  457. + Assert.True(assertsExecuted);
  458. + }
  459. +
  460. [Theory]
  461. [InlineData(0, "h1", "::1", "http")]
  462. [InlineData(1, "", "::1", "http")]