diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs index 9a435392ff..64365e486e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs @@ -49,8 +49,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel case RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName: ex = new BadHttpRequestException("Whitespace is not allowed in header name."); break; - case RequestRejectionReason.HeaderLineMustEndInCRLFOnlyCRFound: - ex = new BadHttpRequestException("Header line must end in CRLF; only CR found."); + case RequestRejectionReason.HeaderValueMustNotContainCR: + ex = new BadHttpRequestException("Header value must not contain CR characters."); break; case RequestRejectionReason.HeaderValueLineFoldingNotSupported: ex = new BadHttpRequestException("Header value line folding not supported."); @@ -91,6 +91,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel case RequestRejectionReason.MissingCrAfterVersion: ex = new BadHttpRequestException("Missing CR in request line."); break; + case RequestRejectionReason.HeadersExceedMaxTotalSize: + ex = new BadHttpRequestException("Request headers too long."); + break; + case RequestRejectionReason.MissingCRInHeaderLine: + ex = new BadHttpRequestException("No CR character found in header line."); + break; + case RequestRejectionReason.TooManyHeaders: + ex = new BadHttpRequestException("Request contains too many headers."); + break; default: ex = new BadHttpRequestException("Bad request."); break; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 70e5bce881..f6415b2bb1 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -66,6 +66,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private readonly string _pathBase; + private int _remainingRequestHeadersBytesAllowed; + private int _requestHeadersParsed; + public Frame(ConnectionContext context) : base(context) { @@ -305,6 +308,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http _manuallySetRequestAbortToken = null; _abortedCts = null; + + _remainingRequestHeadersBytesAllowed = ServerOptions.Limits.MaxRequestHeadersTotalSize; + _requestHeadersParsed = 0; } /// @@ -1083,63 +1089,63 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http RejectRequest(RequestRejectionReason.HeaderLineMustNotStartWithWhitespace); } - var beginName = scan; - if (scan.Seek(ref _vectorColons, ref _vectorCRs) == -1) + // If we've parsed the max allowed numbers of headers and we're starting a new + // one, we've gone over the limit. + if (_requestHeadersParsed == ServerOptions.Limits.MaxRequestHeaderCount) { - return false; - } - var endName = scan; - - ch = scan.Take(); - if (ch != ':') - { - RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine); + RejectRequest(RequestRejectionReason.TooManyHeaders); } - var validateName = beginName; - if (validateName.Seek(ref _vectorSpaces, ref _vectorTabs, ref _vectorColons) != ':') + var end = scan; + int bytesScanned; + if (end.Seek(ref _vectorLFs, out bytesScanned, _remainingRequestHeadersBytesAllowed) == -1) { - RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName); - } - - var beginValue = scan; - ch = scan.Peek(); - - if (ch == -1) - { - return false; - } - - // Skip header value leading whitespace. - while (ch == ' ' || ch == '\t') - { - scan.Take(); - beginValue = scan; - - ch = scan.Peek(); - if (ch == -1) + if (bytesScanned >= _remainingRequestHeadersBytesAllowed) + { + RejectRequest(RequestRejectionReason.HeadersExceedMaxTotalSize); + } + else { return false; } } - scan = beginValue; - if (scan.Seek(ref _vectorCRs) == -1) + var beginName = scan; + if (scan.Seek(ref _vectorColons, ref end) == -1) { - // no "\r" in sight, burn used bytes and go back to await more data - return false; + RejectRequest(RequestRejectionReason.NoColonCharacterFoundInHeaderLine); + } + var endName = scan; + + scan.Take(); + + var validateName = beginName; + if (validateName.Seek(ref _vectorSpaces, ref _vectorTabs, ref endName) != -1) + { + RejectRequest(RequestRejectionReason.WhitespaceIsNotAllowedInHeaderName); + } + + var beginValue = scan; + ch = scan.Take(); + + while (ch == ' ' || ch == '\t') + { + beginValue = scan; + ch = scan.Take(); + } + + scan = beginValue; + if (scan.Seek(ref _vectorCRs, ref end) == -1) + { + RejectRequest(RequestRejectionReason.MissingCRInHeaderLine); } scan.Take(); // we know this is '\r' ch = scan.Take(); // expecting '\n' - if (ch == -1) + if (ch != '\n') { - return false; - } - else if (ch != '\n') - { - RejectRequest(RequestRejectionReason.HeaderLineMustEndInCRLFOnlyCRFound); + RejectRequest(RequestRejectionReason.HeaderValueMustNotContainCR); } var next = scan.Peek(); @@ -1195,6 +1201,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http consumed = scan; requestHeaders.Append(name.Array, name.Offset, name.Count, value); + + _remainingRequestHeadersBytesAllowed -= bytesScanned; + _requestHeadersParsed++; } return false; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs index d71ff5b414..a0d04ed0fc 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http HeaderLineMustNotStartWithWhitespace, NoColonCharacterFoundInHeaderLine, WhitespaceIsNotAllowedInHeaderName, - HeaderLineMustEndInCRLFOnlyCRFound, + HeaderValueMustNotContainCR, HeaderValueLineFoldingNotSupported, MalformedRequestLineStatus, MalformedRequestInvalidHeaders, @@ -31,5 +31,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http MissingSpaceAfterMethod, MissingSpaceAfterTarget, MissingCrAfterVersion, + HeadersExceedMaxTotalSize, + MissingCRInHeaderLine, + TooManyHeaders, } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs index 02afceca2b..d59bcd75e9 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -14,6 +14,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel // Matches the default large_client_header_buffers in nginx. private int _maxRequestLineSize = 8 * 1024; + // Matches the default large_client_header_buffers in nginx. + private int _maxRequestHeadersTotalSize = 32 * 1024; + + // Matches the default LimitRequestFields in Apache httpd. + private int _maxRequestHeaderCount = 100; + /// /// Gets or sets the maximum size of the request buffer. /// @@ -58,5 +64,49 @@ namespace Microsoft.AspNetCore.Server.Kestrel _maxRequestLineSize = value; } } + + /// + /// Gets or sets the maximum allowed size for the HTTP request headers. + /// + /// + /// Defaults to 32,768 bytes (32 KB). + /// + public int MaxRequestHeadersTotalSize + { + get + { + return _maxRequestHeadersTotalSize; + } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must a positive integer."); + } + _maxRequestHeadersTotalSize = value; + } + } + + /// + /// Gets or sets the maximum allowed number of headers per HTTP request. + /// + /// + /// Defaults to 100. + /// + public int MaxRequestHeaderCount + { + get + { + return _maxRequestHeaderCount; + } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must a positive integer."); + } + _maxRequestHeaderCount = value; + } + } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs new file mode 100644 index 0000000000..2c7eec616e --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestHeaderLimitsTests.cs @@ -0,0 +1,140 @@ +// 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.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class RequestHeaderLimitsTests + { + [Theory] + [InlineData(0, 1)] + [InlineData(0, 1337)] + [InlineData(1, 0)] + [InlineData(1, 1)] + [InlineData(1, 1337)] + [InlineData(5, 0)] + [InlineData(5, 1)] + [InlineData(5, 1337)] + public async Task ServerAcceptsRequestWithHeaderTotalSizeWithinLimit(int headerCount, int extraLimit) + { + var headers = MakeHeaders(headerCount); + + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestHeadersTotalSize = headers.Length + extraLimit; + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); + await connection.Receive($"HTTP/1.1 200 OK\r\n"); + } + } + } + + [Theory] + [InlineData(0, 1)] + [InlineData(0, 1337)] + [InlineData(1, 1)] + [InlineData(1, 2)] + [InlineData(1, 1337)] + [InlineData(5, 5)] + [InlineData(5, 6)] + [InlineData(5, 1337)] + public async Task ServerAcceptsRequestWithHeaderCountWithinLimit(int headerCount, int maxHeaderCount) + { + var headers = MakeHeaders(headerCount); + + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestHeaderCount = maxHeaderCount; + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); + await connection.Receive($"HTTP/1.1 200 OK\r\n"); + } + } + } + + [Theory] + [InlineData(1)] + [InlineData(5)] + public async Task ServerRejectsRequestWithHeaderTotalSizeOverLimit(int headerCount) + { + var headers = MakeHeaders(headerCount); + + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestHeadersTotalSize = headers.Length - 1; + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendAllTryEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); + await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + } + } + } + + [Theory] + [InlineData(2, 1)] + [InlineData(5, 1)] + [InlineData(5, 4)] + public async Task ServerRejectsRequestWithHeaderCountOverLimit(int headerCount, int maxHeaderCount) + { + var headers = MakeHeaders(headerCount); + + using (var host = BuildWebHost(options => + { + options.Limits.MaxRequestHeaderCount = maxHeaderCount; + })) + { + host.Start(); + + using (var connection = new TestConnection(host.GetPort())) + { + await connection.SendAllTryEnd($"GET / HTTP/1.1\r\n{headers}\r\n"); + await connection.Receive($"HTTP/1.1 400 Bad Request\r\n"); + } + } + } + + private static string MakeHeaders(int count) + { + return string.Join("", Enumerable + .Range(0, count) + .Select(i => $"Header-{i}: value{i}\r\n")); + } + + private static IWebHost BuildWebHost(Action options) + { + var host = new WebHostBuilder() + .UseKestrel(options) + .UseUrls("http://127.0.0.1:0/") + .Configure(app => app.Run(async context => + { + await context.Response.WriteAsync("hello, world"); + })) + .Build(); + + return host; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs index e3ef2109df..c6de0ab0e3 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/ChunkedRequestTests.cs @@ -255,6 +255,89 @@ namespace Microsoft.AspNetCore.Server.KestrelTests } } + [Theory] + [MemberData(nameof(ConnectionFilterData))] + public async Task TrailingHeadersCountTowardsHeadersTotalSizeLimit(TestServiceContext testContext) + { + const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; + const string headerLine = "Header: value"; + const string trailingHeaderLine = "Trailing-Header: trailing-value"; + + testContext.ServerOptions.Limits.MaxRequestHeadersTotalSize = + transferEncodingHeaderLine.Length + 2 + + headerLine.Length + 2 + + trailingHeaderLine.Length + 1; + + using (var server = new TestServer(async context => + { + var buffer = new byte[128]; + while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAllTryEnd( + "POST / HTTP/1.1", + $"{transferEncodingHeaderLine}", + $"{headerLine}", + "", + "2", + "42", + "0", + $"{trailingHeaderLine}", + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 400 Bad Request", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Theory] + [MemberData(nameof(ConnectionFilterData))] + public async Task TrailingHeadersCountTowardsHeaderCountLimit(TestServiceContext testContext) + { + const string transferEncodingHeaderLine = "Transfer-Encoding: chunked"; + const string headerLine = "Header: value"; + const string trailingHeaderLine = "Trailing-Header: trailing-value"; + + testContext.ServerOptions.Limits.MaxRequestHeaderCount = 2; + + using (var server = new TestServer(async context => + { + var buffer = new byte[128]; + while (await context.Request.Body.ReadAsync(buffer, 0, buffer.Length) != 0) ; // read to end + }, testContext)) + { + using (var connection = server.CreateConnection()) + { + await connection.SendAllTryEnd( + "POST / HTTP/1.1", + $"{transferEncodingHeaderLine}", + $"{headerLine}", + "", + "2", + "42", + "0", + $"{trailingHeaderLine}", + "", + ""); + await connection.ReceiveForcedEnd( + "HTTP/1.1 400 Bad Request", + "Connection: close", + $"Date: {testContext.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + [Theory] [MemberData(nameof(ConnectionFilterData))] public async Task ExtensionsAreIgnored(TestServiceContext testContext) diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs index 7b1af2b94f..5a0492ea19 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/FrameTests.cs @@ -27,9 +27,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var connectionContext = new ConnectionContext() { DateHeaderValueManager = new DateHeaderValueManager(), - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes("Header:value\r\n\r\n"); @@ -68,9 +70,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var connectionContext = new ConnectionContext() { DateHeaderValueManager = new DateHeaderValueManager(), - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); @@ -109,8 +113,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); @@ -148,8 +154,10 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); @@ -187,23 +195,23 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Header value line folding not supported.", exception.Message); } } - [Theory] - [InlineData("Header-1: value1\r\r\n")] - [InlineData("Header-1: value1\rHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\r\n")] - public void ThrowsOnHeaderLineNotEndingInCRLF(string rawHeaders) + [Fact] + public void ThrowsOnHeaderValueWithLineFolding_CharacterNotAvailableOnFirstAttempt() { var trace = new KestrelTrace(new TestKestrelTrace()); var ltp = new LoggingThreadPool(trace); @@ -214,15 +222,54 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + frame.InitializeHeaders(); + + var headerArray = Encoding.ASCII.GetBytes("Header-1: value1\r\n"); + socketInput.IncomingData(headerArray, 0, headerArray.Length); + + Assert.False(frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + + socketInput.IncomingData(Encoding.ASCII.GetBytes(" "), 0, 1); + + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Header value line folding not supported.", exception.Message); + } + } + + [Theory] + [InlineData("Header-1: value1\r\r\n")] + [InlineData("Header-1: val\rue1\r\n")] + [InlineData("Header-1: value1\rHeader-2: value2\r\n\r\n")] + [InlineData("Header-1: value1\r\nHeader-2: value2\r\r\n")] + [InlineData("Header-1: value1\r\nHeader-2: v\ralue2\r\n")] + public void ThrowsOnHeaderValueContainingCR(string rawHeaders) + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), + Log = trace + }; + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Header value must not contain CR characters.", exception.Message); } } @@ -241,15 +288,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("No ':' character found in header line.", exception.Message); } } @@ -258,8 +308,6 @@ namespace Microsoft.AspNetCore.Server.KestrelTests [InlineData("\tHeader: value\r\n\r\n")] [InlineData(" Header-1: value1\r\nHeader-2: value2\r\n\r\n")] [InlineData("\tHeader-1: value1\r\nHeader-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\n Header-2: value2\r\n\r\n")] - [InlineData("Header-1: value1\r\n\tHeader-2: value2\r\n\r\n")] public void ThrowsOnHeaderLineStartingWithWhitespace(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); @@ -271,15 +319,18 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Header line must not start with whitespace.", exception.Message); } } @@ -303,21 +354,25 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Whitespace is not allowed in header name.", exception.Message); } } [Theory] + [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r\r")] [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r ")] - [InlineData("Header-1: value1\r\nHeader-2: value2\r\nEnd\r\n")] + [InlineData("Header-1: value1\r\nHeader-2: value2\r\n\r \n")] public void ThrowsOnHeadersNotEndingInCRLFLine(string rawHeaders) { var trace = new KestrelTrace(new TestKestrelTrace()); @@ -329,15 +384,84 @@ namespace Microsoft.AspNetCore.Server.KestrelTests { DateHeaderValueManager = new DateHeaderValueManager(), ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), Log = trace }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); socketInput.IncomingData(headerArray, 0, headerArray.Length); - Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Headers corrupted, invalid header sequence.", exception.Message); + } + } + + [Fact] + public void ThrowsWhenHeadersExceedTotalSizeLimit() + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + const string headerLine = "Header: value\r\n"; + + var options = new KestrelServerOptions(); + options.Limits.MaxRequestHeadersTotalSize = headerLine.Length - 1; + + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = options, + Log = trace + }; + + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + frame.InitializeHeaders(); + + var headerArray = Encoding.ASCII.GetBytes($"{headerLine}\r\n"); + socketInput.IncomingData(headerArray, 0, headerArray.Length); + + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Request headers too long.", exception.Message); + } + } + + [Fact] + public void ThrowsWhenHeadersExceedCountLimit() + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + const string headerLines = "Header-1: value1\r\nHeader-2: value2\r\n"; + + var options = new KestrelServerOptions(); + options.Limits.MaxRequestHeaderCount = 1; + + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = options, + Log = trace + }; + + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + frame.InitializeHeaders(); + + var headerArray = Encoding.ASCII.GetBytes($"{headerLines}\r\n"); + socketInput.IncomingData(headerArray, 0, headerArray.Length); + + var exception = Assert.Throws(() => frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal("Request contains too many headers.", exception.Message); } } @@ -358,9 +482,11 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var connectionContext = new ConnectionContext() { DateHeaderValueManager = new DateHeaderValueManager(), - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); frame.InitializeHeaders(); var headerArray = Encoding.ASCII.GetBytes(rawHeaders); @@ -384,7 +510,8 @@ namespace Microsoft.AspNetCore.Server.KestrelTests var connectionContext = new ConnectionContext() { DateHeaderValueManager = new DateHeaderValueManager(), - ServerAddress = ServerAddress.FromUrl("http://localhost:5000") + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = new KestrelServerOptions(), }; var frame = new Frame(application: null, context: connectionContext); frame.Scheme = "https"; @@ -396,6 +523,50 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal("http", ((IFeatureCollection)frame).Get().Scheme); } + [Fact] + public void ResetResetsHeaderLimits() + { + var trace = new KestrelTrace(new TestKestrelTrace()); + var ltp = new LoggingThreadPool(trace); + using (var pool = new MemoryPool()) + using (var socketInput = new SocketInput(pool, ltp)) + { + const string headerLine1 = "Header-1: value1\r\n"; + const string headerLine2 = "Header-2: value2\r\n"; + + var options = new KestrelServerOptions(); + options.Limits.MaxRequestHeadersTotalSize = headerLine1.Length; + options.Limits.MaxRequestHeaderCount = 1; + + var connectionContext = new ConnectionContext() + { + DateHeaderValueManager = new DateHeaderValueManager(), + ServerAddress = ServerAddress.FromUrl("http://localhost:5000"), + ServerOptions = options + }; + + var frame = new Frame(application: null, context: connectionContext); + frame.Reset(); + frame.InitializeHeaders(); + + var headerArray1 = Encoding.ASCII.GetBytes($"{headerLine1}\r\n"); + socketInput.IncomingData(headerArray1, 0, headerArray1.Length); + + Assert.True(frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal(1, frame.RequestHeaders.Count); + Assert.Equal("value1", frame.RequestHeaders["Header-1"]); + + frame.Reset(); + + var headerArray2 = Encoding.ASCII.GetBytes($"{headerLine2}\r\n"); + socketInput.IncomingData(headerArray2, 0, headerArray1.Length); + + Assert.True(frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)); + Assert.Equal(1, frame.RequestHeaders.Count); + Assert.Equal("value2", frame.RequestHeaders["Header-2"]); + } + } + [Fact] public void ThrowsWhenStatusCodeIsSetAfterResponseStarted() { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs index 15a216130a..111f97193c 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -63,5 +63,61 @@ namespace Microsoft.AspNetCore.Server.KestrelTests o.MaxRequestLineSize = value; Assert.Equal(value, o.MaxRequestLineSize); } + + [Fact] + public void MaxRequestHeaderTotalSizeDefault() + { + Assert.Equal(32 * 1024, (new KestrelServerLimits()).MaxRequestHeadersTotalSize); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-1)] + [InlineData(0)] + public void MaxRequestHeaderTotalSizeInvalid(int value) + { + Assert.Throws(() => + { + (new KestrelServerLimits()).MaxRequestHeadersTotalSize = value; + }); + } + + [Theory] + [InlineData(1)] + [InlineData(int.MaxValue)] + public void MaxRequestHeaderTotalSizeValid(int value) + { + var o = new KestrelServerLimits(); + o.MaxRequestHeadersTotalSize = value; + Assert.Equal(value, o.MaxRequestHeadersTotalSize); + } + + [Fact] + public void MaxRequestHeadersDefault() + { + Assert.Equal(100, (new KestrelServerLimits()).MaxRequestHeaderCount); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-1)] + [InlineData(0)] + public void MaxRequestHeadersInvalid(int value) + { + Assert.Throws(() => + { + (new KestrelServerLimits()).MaxRequestHeaderCount = value; + }); + } + + [Theory] + [InlineData(1)] + [InlineData(int.MaxValue)] + public void MaxRequestHeadersValid(int value) + { + var o = new KestrelServerLimits(); + o.MaxRequestHeaderCount = value; + Assert.Equal(value, o.MaxRequestHeaderCount); + } } }