Scheme and Host value validation
This commit is contained in:
Родитель
8319d8eb0f
Коммит
c9c40d8126
|
@ -5,6 +5,7 @@ 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;
|
||||
|
@ -17,10 +18,49 @@ 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 +219,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
|||
|
||||
if (checkProto)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(set.Scheme))
|
||||
if (!string.IsNullOrEmpty(set.Scheme) && (_options.UseRelaxedHeaderValidation || TryValidateScheme(set.Scheme)))
|
||||
{
|
||||
applyChanges = true;
|
||||
currentValues.Scheme = set.Scheme;
|
||||
|
@ -193,7 +233,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
|||
|
||||
if (checkHost)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(set.Host))
|
||||
if (!string.IsNullOrEmpty(set.Host) && (_options.UseRelaxedHeaderValidation || TryValidateHost(set.Host)))
|
||||
{
|
||||
applyChanges = true;
|
||||
currentValues.Host = set.Host;
|
||||
|
@ -288,5 +328,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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Net;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
|
@ -9,6 +10,16 @@ namespace Microsoft.AspNetCore.Builder
|
|||
{
|
||||
public class ForwardedHeadersOptions
|
||||
{
|
||||
private const string UseRelaxedHeaderValidationSwitch = "Switch.Microsoft.AspNetCore.HttpOverrides.UseRelaxedHeaderValidation";
|
||||
private static readonly bool GlobalUseRelaxedHeaderValidation;
|
||||
|
||||
static ForwardedHeadersOptions()
|
||||
{
|
||||
AppContext.TryGetSwitch(UseRelaxedHeaderValidationSwitch, out GlobalUseRelaxedHeaderValidation);
|
||||
}
|
||||
|
||||
internal bool UseRelaxedHeaderValidation { get; set; } = GlobalUseRelaxedHeaderValidation;
|
||||
|
||||
/// <summary>
|
||||
/// Use this header instead of <see cref="ForwardedHeadersDefaults.XForwardedForHeaderName"/>
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.HttpOverrides.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
|
@ -311,6 +311,223 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
|||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Host", host);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
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
|
||||
"[", // Incomplete
|
||||
"[]", // 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);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Host", host);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string> HostHeaderInvalidAndExpectedData
|
||||
{
|
||||
get
|
||||
{
|
||||
// see https://tools.ietf.org/html/rfc7230#section-5.4
|
||||
var data = new TheoryData<string, string>() {
|
||||
{ "[]", "[]" }, // Too short
|
||||
{ "[::]", "[::]" }, // Too short
|
||||
{ "[ghijkl]", "[ghijkl]" }, // Non-hex
|
||||
{ "[afd:adf:123", "[afd:adf:123" }, // Incomplete
|
||||
{ "[afd:adf]123", "[afd:adf]123" }, // Missing :
|
||||
{ "[afd:adf]:", "[afd:adf]:" }, // Missing port digits
|
||||
{ "[afd adf]", "[afd adf]" }, // Space
|
||||
{ "[ad-314]", "[ad-314]" }, // dash
|
||||
{ ":1234", ":1234" }, // Missing host
|
||||
{ "a:b:c", "a:b:c" }, // Missing []
|
||||
{ "::1", "::1" }, // Missing []
|
||||
{ "::", "::" }, // Missing everything
|
||||
{ "abcd:1abcd", "abcd:1abcd" }, // Letters in port
|
||||
{ "abcd:1.2", "abcd:1.2" }, // Dot in port
|
||||
{ "1.2.3.4:", "1.2.3.4:" }, // Missing port digits
|
||||
{ "1.2 .4", "1.2 .4" }, // Space
|
||||
{ "a:b:c::", "a:b:c::" }, // Incomplete
|
||||
{ "[abd]]:1234", "[abd]]:1234" }, // Incomplete
|
||||
};
|
||||
|
||||
// These aren't allowed anywhere in the host header
|
||||
var invalid = "\"#%*+/;<=>?@[]\\^`{}|";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add(ch.ToString(), ch.ToString());
|
||||
}
|
||||
|
||||
invalid = "!\"#$%&'()*+/;<=>?@[\\^_`{}|~-";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add("[abd" + ch + "]:1234", "[abd" + ch + "]:1234");
|
||||
}
|
||||
|
||||
invalid = "!\"#$%&'()*+/;<=>?@[]\\^_`{}|~abcABC-.";
|
||||
foreach (var ch in invalid)
|
||||
{
|
||||
data.Add("a.b.c:" + ch, "a.b.c:" + ch + "");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(HostHeaderInvalidAndExpectedData))]
|
||||
public async Task XForwardedHostAllowsInvalidValidCharactersWhenRelaxed(string host, string expected)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedHost,
|
||||
UseRelaxedHeaderValidation = true,
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal(expected, context.Request.Host.Value);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Host", host);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "h1", "http")]
|
||||
[InlineData(1, "", "http")]
|
||||
|
@ -346,6 +563,127 @@ namespace Microsoft.AspNetCore.HttpOverrides
|
|||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Proto", scheme);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
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);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Proto", scheme);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ProtoHeaderInvalidData))]
|
||||
public async Task XForwardedProtoAcceptsValidProtocolsIfRelaxed(string scheme)
|
||||
{
|
||||
var assertsExecuted = false;
|
||||
|
||||
var builder = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
|
||||
UseRelaxedHeaderValidation = true,
|
||||
});
|
||||
app.Run(context =>
|
||||
{
|
||||
Assert.Equal(scheme, context.Request.Scheme);
|
||||
assertsExecuted = true;
|
||||
return Task.FromResult(0);
|
||||
});
|
||||
});
|
||||
var server = new TestServer(builder);
|
||||
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "");
|
||||
req.Headers.Add("X-Forwarded-Proto", scheme);
|
||||
await server.CreateClient().SendAsync(req);
|
||||
Assert.True(assertsExecuted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, "h1", "::1", "http")]
|
||||
[InlineData(1, "", "::1", "http")]
|
||||
|
|
Загрузка…
Ссылка в новой задаче