| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- commit 8b58a9a0917315cef4aac6d0ffb44144d9df9bf1
- Author: Chris Ross (ASP.NET) <[email protected]>
- Date: Fri Jan 12 12:58:56 2018 -0800
- Scheme and Host value validation
- diff --git a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
- index 1863087c0ee..d5e074de57d 100644
- --- a/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
- +++ b/src/Microsoft.AspNetCore.HttpOverrides/ForwardedHeadersMiddleware.cs
- @@ -2,25 +2,63 @@
- // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
- using System;
- -using System.Collections.Generic;
- using System.Linq;
- using System.Net;
- +using System.Runtime.CompilerServices;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.HttpOverrides.Internal;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- -using Microsoft.Extensions.Primitives;
-
- namespace Microsoft.AspNetCore.HttpOverrides
- {
- public class ForwardedHeadersMiddleware
- {
- + private static readonly bool[] HostCharValidity = new bool[127];
- + private static readonly bool[] SchemeCharValidity = new bool[123];
- +
- private readonly ForwardedHeadersOptions _options;
- private readonly RequestDelegate _next;
- private readonly ILogger _logger;
-
- + static ForwardedHeadersMiddleware()
- + {
- + // RFC 3986 scheme = ALPHA * (ALPHA / DIGIT / "+" / "-" / ".")
- + SchemeCharValidity['+'] = true;
- + SchemeCharValidity['-'] = true;
- + SchemeCharValidity['.'] = true;
- +
- + // Host Matches Http.Sys and Kestrel
- + // Host Matches RFC 3986 except "*" / "+" / "," / ";" / "=" and "%" HEXDIG HEXDIG which are not allowed by Http.Sys
- + HostCharValidity['!'] = true;
- + HostCharValidity['$'] = true;
- + HostCharValidity['&'] = true;
- + HostCharValidity['\''] = true;
- + HostCharValidity['('] = true;
- + HostCharValidity[')'] = true;
- + HostCharValidity['-'] = true;
- + HostCharValidity['.'] = true;
- + HostCharValidity['_'] = true;
- + HostCharValidity['~'] = true;
- + for (var ch = '0'; ch <= '9'; ch++)
- + {
- + SchemeCharValidity[ch] = true;
- + HostCharValidity[ch] = true;
- + }
- + for (var ch = 'A'; ch <= 'Z'; ch++)
- + {
- + SchemeCharValidity[ch] = true;
- + HostCharValidity[ch] = true;
- + }
- + for (var ch = 'a'; ch <= 'z'; ch++)
- + {
- + SchemeCharValidity[ch] = true;
- + HostCharValidity[ch] = true;
- + }
- + }
- +
- public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ForwardedHeadersOptions> options)
- {
- if (next == null)
- @@ -179,7 +217,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
-
- if (checkProto)
- {
- - if (!string.IsNullOrEmpty(set.Scheme))
- + if (!string.IsNullOrEmpty(set.Scheme) && TryValidateScheme(set.Scheme))
- {
- applyChanges = true;
- currentValues.Scheme = set.Scheme;
- @@ -193,7 +231,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
-
- if (checkHost)
- {
- - if (!string.IsNullOrEmpty(set.Host))
- + if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host))
- {
- applyChanges = true;
- currentValues.Host = set.Host;
- @@ -288,5 +326,124 @@ namespace Microsoft.AspNetCore.HttpOverrides
- public string Host;
- public string Scheme;
- }
- +
- + // Empty was checked for by the caller
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool TryValidateScheme(string scheme)
- + {
- + for (var i = 0; i < scheme.Length; i++)
- + {
- + if (!IsValidSchemeChar(scheme[i]))
- + {
- + return false;
- + }
- + }
- + return true;
- + }
- +
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private static bool IsValidSchemeChar(char ch)
- + {
- + return ch < SchemeCharValidity.Length && SchemeCharValidity[ch];
- + }
- +
- + // Empty was checked for by the caller
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool TryValidateHost(string host)
- + {
- + if (host[0] == '[')
- + {
- + return TryValidateIPv6Host(host);
- + }
- +
- + if (host[0] == ':')
- + {
- + // Only a port
- + return false;
- + }
- +
- + var i = 0;
- + for (; i < host.Length; i++)
- + {
- + if (!IsValidHostChar(host[i]))
- + {
- + break;
- + }
- + }
- + return TryValidateHostPort(host, i);
- + }
- +
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private static bool IsValidHostChar(char ch)
- + {
- + return ch < HostCharValidity.Length && HostCharValidity[ch];
- + }
- +
- + // The lead '[' was already checked
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool TryValidateIPv6Host(string hostText)
- + {
- + for (var i = 1; i < hostText.Length; i++)
- + {
- + var ch = hostText[i];
- + if (ch == ']')
- + {
- + // [::1] is the shortest valid IPv6 host
- + if (i < 4)
- + {
- + return false;
- + }
- + return TryValidateHostPort(hostText, i + 1);
- + }
- +
- + if (!IsHex(ch) && ch != ':' && ch != '.')
- + {
- + return false;
- + }
- + }
- +
- + // Must contain a ']'
- + return false;
- + }
- +
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool TryValidateHostPort(string hostText, int offset)
- + {
- + if (offset == hostText.Length)
- + {
- + // No port
- + return true;
- + }
- +
- + if (hostText[offset] != ':' || hostText.Length == offset + 1)
- + {
- + // Must have at least one number after the colon if present.
- + return false;
- + }
- +
- + for (var i = offset + 1; i < hostText.Length; i++)
- + {
- + if (!IsNumeric(hostText[i]))
- + {
- + return false;
- + }
- + }
- +
- + return true;
- + }
- +
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool IsNumeric(char ch)
- + {
- + return '0' <= ch && ch <= '9';
- + }
- +
- + [MethodImpl(MethodImplOptions.AggressiveInlining)]
- + private bool IsHex(char ch)
- + {
- + return IsNumeric(ch)
- + || ('a' <= ch && ch <= 'f')
- + || ('A' <= ch && ch <= 'F');
- + }
- }
- }
- diff --git a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
- index ea905dace0f..81d6ccf15a0 100644
- --- a/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
- +++ b/test/Microsoft.AspNetCore.HttpOverrides.Tests/ForwardedHeadersMiddlewareTest.cs
- @@ -252,6 +252,146 @@ namespace Microsoft.AspNetCore.HttpOverrides
- Assert.Equal("testhost", context.Request.Host.ToString());
- }
-
- + public static TheoryData<string> HostHeaderData
- + {
- + get
- + {
- + return new TheoryData<string>() {
- + "z",
- + "1",
- + "y:1",
- + "1:1",
- + "[ABCdef]",
- + "[abcDEF]:0",
- + "[abcdef:127.2355.1246.114]:0",
- + "[::1]:80",
- + "127.0.0.1:80",
- + "900.900.900.900:9523547852",
- + "foo",
- + "foo:234",
- + "foo.bar.baz",
- + "foo.BAR.baz:46245",
- + "foo.ba-ar.baz:46245",
- + "-foo:1234",
- + "xn--c1yn36f:134",
- + "-",
- + "_",
- + "~",
- + "!",
- + "$",
- + "'",
- + "(",
- + ")",
- + };
- + }
- + }
- +
- + [Theory]
- + [MemberData(nameof(HostHeaderData))]
- + public async Task XForwardedHostAllowsValidCharacters(string host)
- + {
- + var assertsExecuted = false;
- +
- + var builder = new WebHostBuilder()
- + .Configure(app =>
- + {
- + app.UseForwardedHeaders(new ForwardedHeadersOptions
- + {
- + ForwardedHeaders = ForwardedHeaders.XForwardedHost
- + });
- + app.Run(context =>
- + {
- + Assert.Equal(host, context.Request.Host.ToString());
- + assertsExecuted = true;
- + return Task.FromResult(0);
- + });
- + });
- + var server = new TestServer(builder);
- +
- + await server.SendAsync(c =>
- + {
- + c.Request.Headers["X-Forwarded-Host"] = host;
- + });
- + Assert.True(assertsExecuted);
- + }
- +
- + public static TheoryData<string> HostHeaderInvalidData
- + {
- + get
- + {
- + // see https://tools.ietf.org/html/rfc7230#section-5.4
- + var data = new TheoryData<string>() {
- + "", // Empty
- + "[]", // Too short
- + "[::]", // Too short
- + "[ghijkl]", // Non-hex
- + "[afd:adf:123", // Incomplete
- + "[afd:adf]123", // Missing :
- + "[afd:adf]:", // Missing port digits
- + "[afd adf]", // Space
- + "[ad-314]", // dash
- + ":1234", // Missing host
- + "a:b:c", // Missing []
- + "::1", // Missing []
- + "::", // Missing everything
- + "abcd:1abcd", // Letters in port
- + "abcd:1.2", // Dot in port
- + "1.2.3.4:", // Missing port digits
- + "1.2 .4", // Space
- + };
- +
- + // These aren't allowed anywhere in the host header
- + var invalid = "\"#%*+/;<=>?@[]\\^`{}|";
- + foreach (var ch in invalid)
- + {
- + data.Add(ch.ToString());
- + }
- +
- + invalid = "!\"#$%&'()*+,/;<=>?@[]\\^_`{}|~-";
- + foreach (var ch in invalid)
- + {
- + data.Add("[abd" + ch + "]:1234");
- + }
- +
- + invalid = "!\"#$%&'()*+/;<=>?@[]\\^_`{}|~:abcABC-.";
- + foreach (var ch in invalid)
- + {
- + data.Add("a.b.c:" + ch);
- + }
- +
- + return data;
- + }
- + }
- +
- + [Theory]
- + [MemberData(nameof(HostHeaderInvalidData))]
- + public async Task XForwardedHostFailsForInvalidCharacters(string host)
- + {
- + var assertsExecuted = false;
- +
- + var builder = new WebHostBuilder()
- + .Configure(app =>
- + {
- + app.UseForwardedHeaders(new ForwardedHeadersOptions
- + {
- + ForwardedHeaders = ForwardedHeaders.XForwardedHost
- + });
- + app.Run(context =>
- + {
- + Assert.NotEqual(host, context.Request.Host.Value);
- + assertsExecuted = true;
- + return Task.FromResult(0);
- + });
- + });
- + var server = new TestServer(builder);
- +
- + await server.SendAsync(c =>
- + {
- + c.Request.Headers["X-Forwarded-Host"] = host;
- + });
- + Assert.True(assertsExecuted);
- + }
- +
- [Theory]
- [InlineData(0, "h1", "http")]
- [InlineData(1, "", "http")]
- @@ -281,6 +421,100 @@ namespace Microsoft.AspNetCore.HttpOverrides
- Assert.Equal(expected, context.Request.Scheme);
- }
-
- + public static TheoryData<string> ProtoHeaderData
- + {
- + get
- + {
- + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
- + return new TheoryData<string>() {
- + "z",
- + "Z",
- + "1",
- + "y+",
- + "1-",
- + "a.",
- + };
- + }
- + }
- +
- + [Theory]
- + [MemberData(nameof(ProtoHeaderData))]
- + public async Task XForwardedProtoAcceptsValidProtocols(string scheme)
- + {
- + var assertsExecuted = false;
- +
- + var builder = new WebHostBuilder()
- + .Configure(app =>
- + {
- + app.UseForwardedHeaders(new ForwardedHeadersOptions
- + {
- + ForwardedHeaders = ForwardedHeaders.XForwardedProto
- + });
- + app.Run(context =>
- + {
- + Assert.Equal(scheme, context.Request.Scheme);
- + assertsExecuted = true;
- + return Task.FromResult(0);
- + });
- + });
- + var server = new TestServer(builder);
- +
- + await server.SendAsync(c =>
- + {
- + c.Request.Headers["X-Forwarded-Proto"] = scheme;
- + });
- + Assert.True(assertsExecuted);
- + }
- +
- + public static TheoryData<string> ProtoHeaderInvalidData
- + {
- + get
- + {
- + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
- + var data = new TheoryData<string>() {
- + "a b", // Space
- + };
- +
- + // These aren't allowed anywhere in the scheme header
- + var invalid = "!\"#$%&'()*/:;<=>?@[]\\^_`{}|~";
- + foreach (var ch in invalid)
- + {
- + data.Add(ch.ToString());
- + }
- +
- + return data;
- + }
- + }
- +
- + [Theory]
- + [MemberData(nameof(ProtoHeaderInvalidData))]
- + public async Task XForwardedProtoRejectsInvalidProtocols(string scheme)
- + {
- + var assertsExecuted = false;
- +
- + var builder = new WebHostBuilder()
- + .Configure(app =>
- + {
- + app.UseForwardedHeaders(new ForwardedHeadersOptions
- + {
- + ForwardedHeaders = ForwardedHeaders.XForwardedProto,
- + });
- + app.Run(context =>
- + {
- + Assert.Equal("http", context.Request.Scheme);
- + assertsExecuted = true;
- + return Task.FromResult(0);
- + });
- + });
- + var server = new TestServer(builder);
- +
- + await server.SendAsync(c =>
- + {
- + c.Request.Headers["X-Forwarded-Proto"] = scheme;
- + });
- + Assert.True(assertsExecuted);
- + }
- +
- [Theory]
- [InlineData(0, "h1", "::1", "http")]
- [InlineData(1, "", "::1", "http")]
|