diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs index cb37af76c1..8e70e90c66 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/BadHttpRequestException.cs @@ -78,6 +78,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel case RequestRejectionReason.RequestTimeout: ex = new BadHttpRequestException("Request timed out.", 408); break; + case RequestRejectionReason.PayloadTooLarge: + ex = new BadHttpRequestException("Payload too large.", 413); + break; default: ex = new BadHttpRequestException("Bad request.", 400); break; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs index 0b37975bc3..9966b60c9e 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/Frame.cs @@ -115,7 +115,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private DateHeaderValueManager DateHeaderValueManager => ConnectionContext.ListenerContext.ServiceContext.DateHeaderValueManager; private ServerAddress ServerAddress => ConnectionContext.ListenerContext.ServerAddress; // Hold direct reference to ServerOptions since this is used very often in the request processing path - private KestrelServerOptions ServerOptions { get; } + public KestrelServerOptions ServerOptions { get; } private IPEndPoint LocalEndPoint => ConnectionContext.LocalEndPoint; private IPEndPoint RemoteEndPoint => ConnectionContext.RemoteEndPoint; protected string ConnectionId => ConnectionContext.ConnectionId; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs index 50cb468741..f4a260dcc6 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/MessageBody.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http { @@ -272,6 +271,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http try { var contentLength = FrameHeaders.ParseContentLength(unparsedContentLength); + + if (contentLength > context.ServerOptions.Limits.MaxRequestBodySize) + { + context.RejectRequest(RequestRejectionReason.PayloadTooLarge); + } + return new ForContentLength(keepAlive, contentLength, context); } catch (InvalidOperationException) @@ -281,7 +286,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http } // Avoid slowing down most common case - if (!object.ReferenceEquals(context.Method, HttpMethods.Get)) + if (!ReferenceEquals(context.Method, HttpMethods.Get)) { // If we got here, request contains no Content-Length or Transfer-Encoding header. // Reject with 411 Length Required. @@ -394,6 +399,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http private readonly SocketInput _input; private readonly FrameRequestHeaders _requestHeaders; + private long _inputBytesRead; private int _inputLength; private Mode _mode = Mode.Prefix; @@ -414,6 +420,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http protected override void OnConsumedBytes(int count) { _inputLength -= count; + _inputBytesRead += count; + + if (_inputBytesRead > _context.ServerOptions.Limits.MaxRequestBodySize) + { + _context.RejectRequest(RequestRejectionReason.PayloadTooLarge); + } } private async Task> PeekStateMachineAsync() diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs index 0c05a6b8ea..f774e5d6a3 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/RequestRejectionReason.cs @@ -30,5 +30,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal.Http FinalTransferCodingNotChunked, LengthRequired, LengthRequiredHttp10, + PayloadTooLarge } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs index ac8a047542..b85e3875d8 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel/KestrelServerLimits.cs @@ -23,6 +23,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel // Matches the default LimitRequestFields in Apache httpd. private int _maxRequestHeaderCount = 100; + // Matches default HttpRuntimeSection.MaxRequestLength. + // https://msdn.microsoft.com/en-us/library/system.web.configuration.httpruntimesection.maxrequestlength%28v=vs.100%29.aspx + private long _maxRequestBodySize = 4 * 1024 * 1024; + // Matches the default http.sys connectionTimeout. private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2); @@ -144,6 +148,28 @@ namespace Microsoft.AspNetCore.Server.Kestrel } } + /// + /// Gets or sets the maximum request body size, in bytes. + /// + /// + /// Defaults to 4MB. + /// + public long MaxRequestBodySize + { + get + { + return _maxRequestBodySize; + } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value must a positive integer."); + } + _maxRequestBodySize = value; + } + } + /// /// Gets or sets the keep-alive timeout. /// diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs index ee4a72cc57..77adb2b983 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/KeepAliveTimeoutTests.cs @@ -178,7 +178,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests AddServerHeader = false, Limits = { - KeepAliveTimeout = KeepAliveTimeout + KeepAliveTimeout = KeepAliveTimeout, + // Prevent request rejection if ConnectionNotTimedOutWhileRequestBeingSent + // sends more bytes than the default body size limit. + MaxRequestBodySize = long.MaxValue } } }); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs new file mode 100644 index 0000000000..c54c9b2ad5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBodySizeTests.cs @@ -0,0 +1,168 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class MaxRequestBodySizeTests + { + private const int MaxRequestBodySize = 128; + + [Theory] + [InlineData(MaxRequestBodySize - 1, 0)] + [InlineData(MaxRequestBodySize, 0)] + [InlineData(MaxRequestBodySize - 1, 1)] + [InlineData(MaxRequestBodySize, 1)] + [InlineData(MaxRequestBodySize - 1, 2)] + [InlineData(MaxRequestBodySize, 2)] + public async Task ServerAcceptsRequestBodyWithinLimit(int requestBodySize, int chunks) + { + using (var server = CreateServer(MaxRequestBodySize, async httpContext => await httpContext.Response.WriteAsync("hello, world"))) + { + using (var connection = new TestConnection(server.Port)) + { + await connection.SendAll(BuildRequest(connection, requestBodySize, chunks)); + + await connection.Receive( + "HTTP/1.1 200 OK", + $"Date: {server.Context.DateHeaderValue}", + "Transfer-Encoding: chunked", + "", + "c", + "hello, world", + "0", + "", + ""); + } + } + } + + [Theory] + [InlineData(MaxRequestBodySize + 1, 0)] + [InlineData(MaxRequestBodySize + 1, 1)] + [InlineData(MaxRequestBodySize + 1, 2)] + public async Task ServerRejectsRequestBodyExceedingLimit(int requestBodySize, int chunks) + { + using (var server = CreateServer(MaxRequestBodySize, async httpContext => + { + var received = 0; + while (received < requestBodySize) + { + received += await httpContext.Request.Body.ReadAsync(new byte[1024], 0, 1024); + } + + // Should never get here + await httpContext.Response.WriteAsync("hello, world"); + })) + { + using (var connection = new TestConnection(server.Port)) + { + await connection.SendAll(BuildRequest(connection, requestBodySize, chunks)); + + await connection.ReceiveForcedEnd( + "HTTP/1.1 413 Payload Too Large", + "Connection: close", + $"Date: {server.Context.DateHeaderValue}", + "Content-Length: 0", + "", + ""); + } + } + } + + [Fact] + public async Task MaxRequestBodySizeNotEnforcedOnUpgradedConnection() + { + var sendBytes = MaxRequestBodySize + 1; + + using (var server = CreateServer(MaxRequestBodySize, async httpContext => + { + var stream = await httpContext.Features.Get().UpgradeAsync(); + + var received = 0; + while (received < sendBytes) + { + received += await stream.ReadAsync(new byte[1024], 0, 1024); + } + + var response = Encoding.ASCII.GetBytes($"{received}"); + await stream.WriteAsync(response, 0, response.Length); + })) + { + using (var connection = new TestConnection(server.Port)) + { + await connection.Send( + "GET / HTTP/1.1", + "Connection: upgrade", + "", + new string('a', sendBytes)); + + await connection.Receive( + "HTTP/1.1 101 Switching Protocols", + "Connection: Upgrade", + $"Date: {server.Context.DateHeaderValue}", + "", + $"{sendBytes}"); + } + } + } + + private string BuildRequest(TestConnection connection, int requestBodySize, int chunks) + { + var request = new StringBuilder(); + + request.Append("POST / HTTP/1.1\r\n"); + + if (chunks == 0) + { + request.Append($"Content-Length: {requestBodySize}\r\n\r\n"); + request.Append(new string('a', requestBodySize)); + } + else + { + request.Append("Transfer-Encoding: chunked\r\n\r\n"); + + var bytesSent = 0; + while (bytesSent < requestBodySize) + { + var chunkSize = Math.Min(requestBodySize / chunks, requestBodySize - bytesSent); + + request.Append($"{chunkSize:X}\r\n"); + request.Append(new string('a', chunkSize)); + request.Append("\r\n"); + + bytesSent += chunkSize; + } + + // Make sure we sent the right amount of data + Assert.Equal(requestBodySize, bytesSent); + + request.Append("0\r\n\r\n"); + } + + return request.ToString(); + } + + private TestServer CreateServer(int maxRequestBodySize, RequestDelegate app) + { + return new TestServer(app, new TestServiceContext + { + ServerOptions = new KestrelServerOptions + { + AddServerHeader = false, + Limits = + { + MaxRequestBodySize = maxRequestBodySize + } + } + }); + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs index d78f60a7cd..4a19b8cd10 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/MaxRequestBufferSizeTests.cs @@ -169,6 +169,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests .UseKestrel(options => { options.Limits.MaxRequestBufferSize = maxRequestBufferSize; + options.Limits.MaxRequestBodySize = _dataLength; options.UseHttps(@"TestResources/testCert.pfx", "testPassword"); if (maxRequestBufferSize.HasValue && diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs index 645f7c5205..9434429926 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/RequestTests.cs @@ -43,7 +43,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests Assert.True(bufferLength % 256 == 0, $"{nameof(bufferLength)} must be evenly divisible by 256"); var builder = new WebHostBuilder() - .UseKestrel() + .UseKestrel(options => + { + options.Limits.MaxRequestBodySize = contentLength; + }) .UseUrls("http://127.0.0.1:0/") .Configure(app => { diff --git a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs index 0b9363b58f..ae17670ba3 100644 --- a/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs +++ b/test/Microsoft.AspNetCore.Server.KestrelTests/KestrelServerLimitsTests.cs @@ -149,6 +149,34 @@ namespace Microsoft.AspNetCore.Server.KestrelTests Assert.Equal(value, o.MaxRequestHeaderCount); } + [Fact] + public void MaxRequestBodySizeDefault() + { + Assert.Equal(4 * 1024 * 1024, (new KestrelServerLimits()).MaxRequestBodySize); + } + + [Theory] + [InlineData(long.MinValue)] + [InlineData(-1)] + [InlineData(0)] + public void MaxRequestBodySizeInvalid(long value) + { + Assert.Throws(() => + { + (new KestrelServerLimits()).MaxRequestBodySize = value; + }); + } + + [Theory] + [InlineData(1)] + [InlineData(long.MaxValue)] + public void MaxRequestBodySizeValid(long value) + { + var o = new KestrelServerLimits(); + o.MaxRequestBodySize = value; + Assert.Equal(value, o.MaxRequestBodySize); + } + [Fact] public void KeepAliveTimeoutDefault() {