Scheme and Host value validation

This commit is contained in:
Chris Ross (ASP.NET) 2018-01-12 12:58:56 -08:00
Родитель 8319d8eb0f
Коммит c9c40d8126
4 изменённых файлов: 516 добавлений и 2 удалений

Просмотреть файл

@ -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")]