Scheme and Host value validation

This commit is contained in:
Chris Ross (ASP.NET) 2018-01-12 12:58:56 -08:00
parent 47c1e2ccdf
commit 8b58a9a091
2 changed files with 395 additions and 4 deletions

View File

@ -2,25 +2,63 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides.Internal; using Microsoft.AspNetCore.HttpOverrides.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.HttpOverrides namespace Microsoft.AspNetCore.HttpOverrides
{ {
public class ForwardedHeadersMiddleware 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 ForwardedHeadersOptions _options;
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger _logger; 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) public ForwardedHeadersMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<ForwardedHeadersOptions> options)
{ {
if (next == null) if (next == null)
@ -179,7 +217,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
if (checkProto) if (checkProto)
{ {
if (!string.IsNullOrEmpty(set.Scheme)) if (!string.IsNullOrEmpty(set.Scheme) && TryValidateScheme(set.Scheme))
{ {
applyChanges = true; applyChanges = true;
currentValues.Scheme = set.Scheme; currentValues.Scheme = set.Scheme;
@ -193,7 +231,7 @@ namespace Microsoft.AspNetCore.HttpOverrides
if (checkHost) if (checkHost)
{ {
if (!string.IsNullOrEmpty(set.Host)) if (!string.IsNullOrEmpty(set.Host) && TryValidateHost(set.Host))
{ {
applyChanges = true; applyChanges = true;
currentValues.Host = set.Host; currentValues.Host = set.Host;
@ -288,5 +326,124 @@ namespace Microsoft.AspNetCore.HttpOverrides
public string Host; public string Host;
public string Scheme; 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');
}
} }
} }

View File

@ -252,6 +252,146 @@ namespace Microsoft.AspNetCore.HttpOverrides
Assert.Equal("testhost", context.Request.Host.ToString()); 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] [Theory]
[InlineData(0, "h1", "http")] [InlineData(0, "h1", "http")]
[InlineData(1, "", "http")] [InlineData(1, "", "http")]
@ -281,6 +421,100 @@ namespace Microsoft.AspNetCore.HttpOverrides
Assert.Equal(expected, context.Request.Scheme); 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] [Theory]
[InlineData(0, "h1", "::1", "http")] [InlineData(0, "h1", "::1", "http")]
[InlineData(1, "", "::1", "http")] [InlineData(1, "", "::1", "http")]