From 23b7d8f62a2515e2dbea436ef118fb67e759e2d9 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Tue, 12 Sep 2017 17:56:29 -0700 Subject: [PATCH] Added RequestFormLimits filter. [Fixes #5128] Overriding Request Form max upload limit --- .../MvcCoreServiceCollectionExtensions.cs | 3 +- .../IRequestFormLimitsPolicy.cs | 14 ++ .../Internal/MvcCoreLoggerExtensions.cs | 24 +++ .../Internal/RequestFormLimitsFilter.cs | 52 ++++++ .../RequestFormLimitsAttribute.cs | 154 ++++++++++++++++ .../Internal/RequestFormLimitsFilterTest.cs | 142 +++++++++++++++ .../RequestFormLimitsAttributeTest.cs | 91 ++++++++++ .../RequestFormLimitsTest.cs | 170 ++++++++++++++++++ .../RequestFormLimitsController.cs | 59 ++++++ 9 files changed, 708 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/IRequestFormLimitsPolicy.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/RequestFormLimitsFilter.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/RequestFormLimitsAttribute.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RequestFormLimitsFilterTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/RequestFormLimitsAttributeTest.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestFormLimitsTest.cs create mode 100644 test/WebSites/BasicWebSite/Controllers/RequestFormLimitsController.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index cb00dfca6c..6fe3f0f254 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -199,10 +199,11 @@ namespace Microsoft.Extensions.DependencyInjection ServiceDescriptor.Singleton()); // - // RequestSizeLimit filters + // Request body limit filters // services.TryAddTransient(); services.TryAddTransient(); + services.TryAddTransient(); // Error description services.TryAddSingleton(); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/IRequestFormLimitsPolicy.cs b/src/Microsoft.AspNetCore.Mvc.Core/IRequestFormLimitsPolicy.cs new file mode 100644 index 0000000000..c55608d59d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/IRequestFormLimitsPolicy.cs @@ -0,0 +1,14 @@ +// 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 Microsoft.AspNetCore.Mvc.Filters; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// A marker interface for filters which define a policy for limits on a request's body read as a form. + /// + public interface IRequestFormLimitsPolicy : IFilterMetadata + { + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs index 241e4c92e9..c50d9b5a22 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreLoggerExtensions.cs @@ -77,6 +77,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal private static readonly Action _maxRequestBodySizeSet; private static readonly Action _requestBodySizeLimitDisabled; + private static readonly Action _cannotApplyRequestFormLimits; + private static readonly Action _appliedRequestFormLimits; + + static MvcCoreLoggerExtensions() { _actionExecuting = LoggerMessage.Define( @@ -268,6 +272,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal LogLevel.Debug, 3, "The request body size limit has been disabled."); + + _cannotApplyRequestFormLimits = LoggerMessage.Define( + LogLevel.Warning, + 1, + "Unable to apply configured form options since the request form has already been read."); + + _appliedRequestFormLimits = LoggerMessage.Define( + LogLevel.Debug, + 2, + "Applied the configured form options on the current request."); } public static IDisposable ActionScope(this ILogger logger, ActionDescriptor action) @@ -568,6 +582,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal _requestBodySizeLimitDisabled(logger, null); } + public static void CannotApplyRequestFormLimits(this ILogger logger) + { + _cannotApplyRequestFormLimits(logger, null); + } + + public static void AppliedRequestFormLimits(this ILogger logger) + { + _appliedRequestFormLimits(logger, null); + } + private class ActionLogScope : IReadOnlyList> { private readonly ActionDescriptor _action; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/RequestFormLimitsFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RequestFormLimitsFilter.cs new file mode 100644 index 0000000000..62c31d53af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/RequestFormLimitsFilter.cs @@ -0,0 +1,52 @@ +// 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.Diagnostics; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + /// + /// A filter that configures for the current request. + /// + public class RequestFormLimitsFilter : IAuthorizationFilter, IRequestFormLimitsPolicy + { + private readonly ILogger _logger; + + public RequestFormLimitsFilter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + public FormOptions FormOptions { get; set; } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.IsEffectivePolicy(this)) + { + var features = context.HttpContext.Features; + var formFeature = features.Get(); + + if (formFeature == null || formFeature.Form == null) + { + // Request form has not been read yet, so set the limits + features.Set(new FormFeature(context.HttpContext.Request, FormOptions)); + _logger.AppliedRequestFormLimits(); + } + else + { + _logger.CannotApplyRequestFormLimits(); + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/RequestFormLimitsAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/RequestFormLimitsAttribute.cs new file mode 100644 index 0000000000..d1f1c73578 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/RequestFormLimitsAttribute.cs @@ -0,0 +1,154 @@ +// 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.IO; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Sets the specified limits to the . + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequestFormLimitsAttribute : Attribute, IFilterFactory, IOrderedFilter + { + /// + /// Gets the order value for determining the order of execution of filters. Filters execute in + /// ascending numeric value of the property. + /// + /// + /// + /// Filters are executed in an ordering determined by an ascending sort of the property. + /// + /// + /// The default Order for this attribute is 900 because it must run before ValidateAntiForgeryTokenAttribute and + /// after any filter which does authentication or login in order to allow them to behave as expected (ie Unauthenticated or Redirect instead of 400). + /// + /// + /// Look at for more detailed info. + /// + /// + public int Order { get; set; } = 900; + + /// + public bool IsReusable => true; + + // Internal for unit testing + internal FormOptions FormOptions { get; } = new FormOptions(); + + /// + /// Enables full request body buffering. Use this if multiple components need to read the raw stream. + /// The default value is false. + /// + public bool BufferBody + { + get => FormOptions.BufferBody; + set => FormOptions.BufferBody = value; + } + + /// + /// If is enabled, this many bytes of the body will be buffered in memory. + /// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead. + /// This also applies when buffering individual multipart section bodies. + /// + public int MemoryBufferThreshold + { + get => FormOptions.MemoryBufferThreshold; + set => FormOptions.MemoryBufferThreshold = value; + } + + /// + /// If is enabled, this is the limit for the total number of bytes that will + /// be buffered. Forms that exceed this limit will throw an when parsed. + /// + public long BufferBodyLengthLimit + { + get => FormOptions.BufferBodyLengthLimit; + set => FormOptions.BufferBodyLengthLimit = value; + } + + /// + /// A limit for the number of form entries to allow. + /// Forms that exceed this limit will throw an when parsed. + /// + public int ValueCountLimit + { + get => FormOptions.ValueCountLimit; + set => FormOptions.ValueCountLimit = value; + } + + /// + /// A limit on the length of individual keys. Forms containing keys that exceed this limit will + /// throw an when parsed. + /// + public int KeyLengthLimit + { + get => FormOptions.KeyLengthLimit; + set => FormOptions.KeyLengthLimit = value; + } + + /// + /// A limit on the length of individual form values. Forms containing values that exceed this + /// limit will throw an when parsed. + /// + public int ValueLengthLimit + { + get => FormOptions.ValueLengthLimit; + set => FormOptions.ValueLengthLimit = value; + } + + /// + /// A limit for the length of the boundary identifier. Forms with boundaries that exceed this + /// limit will throw an when parsed. + /// + public int MultipartBoundaryLengthLimit + { + get => FormOptions.MultipartBoundaryLengthLimit; + set => FormOptions.MultipartBoundaryLengthLimit = value; + } + + /// + /// A limit for the number of headers to allow in each multipart section. Headers with the same name will + /// be combined. Form sections that exceed this limit will throw an + /// when parsed. + /// + public int MultipartHeadersCountLimit + { + get => FormOptions.MultipartHeadersCountLimit; + set => FormOptions.MultipartHeadersCountLimit = value; + } + + /// + /// A limit for the total length of the header keys and values in each multipart section. + /// Form sections that exceed this limit will throw an when parsed. + /// + public int MultipartHeadersLengthLimit + { + get => FormOptions.MultipartHeadersLengthLimit; + set => FormOptions.MultipartHeadersLengthLimit = value; + } + + /// + /// A limit for the length of each multipart body. Forms sections that exceed this limit will throw an + /// when parsed. + /// + public long MultipartBodyLengthLimit + { + get => FormOptions.MultipartBodyLengthLimit; + set => FormOptions.MultipartBodyLengthLimit = value; + } + + /// + public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) + { + var filter = serviceProvider.GetRequiredService(); + filter.FormOptions = FormOptions; + return filter; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RequestFormLimitsFilterTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RequestFormLimitsFilterTest.cs new file mode 100644 index 0000000000..a36dd060e4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/RequestFormLimitsFilterTest.cs @@ -0,0 +1,142 @@ +// 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 Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class RequestFormLimitsFilterTest + { + [Fact] + public void SetsRequestFormFeature_WhenFeatureIsNotPresent() + { + // Arrange + var requestFormLimitsFilter = new RequestFormLimitsFilter(NullLoggerFactory.Instance); + requestFormLimitsFilter.FormOptions = new FormOptions(); + var authorizationFilterContext = CreateauthorizationFilterContext( + new IFilterMetadata[] { requestFormLimitsFilter }); + // Set to null explicitly as we want to make sure the filter adds one + authorizationFilterContext.HttpContext.Features.Set(null); + + // Act + requestFormLimitsFilter.OnAuthorization(authorizationFilterContext); + + // Assert + var formFeature = authorizationFilterContext.HttpContext.Features.Get(); + Assert.IsType(formFeature); + } + + [Fact] + public void SetsRequestFormFeature_WhenFeatureIsPresent_ButFormIsNull() + { + // Arrange + var requestFormLimitsFilter = new RequestFormLimitsFilter(NullLoggerFactory.Instance); + requestFormLimitsFilter.FormOptions = new FormOptions(); + var authorizationFilterContext = CreateauthorizationFilterContext( + new IFilterMetadata[] { requestFormLimitsFilter }); + var oldFormFeature = new FormFeature(authorizationFilterContext.HttpContext.Request); + // Set to null explicitly as we want to make sure the filter adds one + authorizationFilterContext.HttpContext.Features.Set(oldFormFeature); + + // Act + requestFormLimitsFilter.OnAuthorization(authorizationFilterContext); + + // Assert + var actualFormFeature = authorizationFilterContext.HttpContext.Features.Get(); + Assert.NotSame(oldFormFeature, actualFormFeature); + } + + [Fact] + public void LogsCannotApplyRequestFormLimits() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var requestFormLimitsFilter = new RequestFormLimitsFilter(loggerFactory); + requestFormLimitsFilter.FormOptions = new FormOptions(); + var authorizationFilterContext = CreateauthorizationFilterContext( + new IFilterMetadata[] { requestFormLimitsFilter }); + authorizationFilterContext.HttpContext.Request.Form = new FormCollection(null); + + // Act + requestFormLimitsFilter.OnAuthorization(authorizationFilterContext); + + // Assert + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Warning, write.LogLevel); + Assert.Equal( + "Unable to apply configured form options since the request form has already been read.", + write.State.ToString()); + } + + [Fact] + public void LogsAppliedRequestFormLimits_WhenFormFeatureIsNull() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var requestFormLimitsFilter = new RequestFormLimitsFilter(loggerFactory); + requestFormLimitsFilter.FormOptions = new FormOptions(); + var authorizationFilterContext = CreateauthorizationFilterContext( + new IFilterMetadata[] { requestFormLimitsFilter }); + // Set to null explicitly as we want to make sure the filter adds one + authorizationFilterContext.HttpContext.Features.Set(null); + + // Act + requestFormLimitsFilter.OnAuthorization(authorizationFilterContext); + + // Assert + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Debug, write.LogLevel); + Assert.Equal( + "Applied the configured form options on the current request.", + write.State.ToString()); + } + + [Fact] + public void LogsAppliedRequestFormLimits_WhenFormFeatureIsPresent_ButFormIsNull() + { + // Arrange + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var requestFormLimitsFilter = new RequestFormLimitsFilter(loggerFactory); + requestFormLimitsFilter.FormOptions = new FormOptions(); + var authorizationFilterContext = CreateauthorizationFilterContext( + new IFilterMetadata[] { requestFormLimitsFilter }); + // Set to null explicitly as we want to make sure the filter adds one + authorizationFilterContext.HttpContext.Features.Set( + new FormFeature(authorizationFilterContext.HttpContext.Request)); + + // Act + requestFormLimitsFilter.OnAuthorization(authorizationFilterContext); + + // Assert + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Debug, write.LogLevel); + Assert.Equal( + "Applied the configured form options on the current request.", + write.State.ToString()); + } + + private static AuthorizationFilterContext CreateauthorizationFilterContext(IFilterMetadata[] filters) + { + return new AuthorizationFilterContext(CreateActionContext(), filters); + } + + private static ActionContext CreateActionContext() + { + return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/RequestFormLimitsAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/RequestFormLimitsAttributeTest.cs new file mode 100644 index 0000000000..8b8cbfa48d --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/RequestFormLimitsAttributeTest.cs @@ -0,0 +1,91 @@ +// 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.Reflection; +using Microsoft.AspNetCore.Http.Features; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class RequestFormLimitsAttributeTest + { + [Fact] + public void AllPublicProperties_OfFormOptions_AreExposed() + { + // Arrange + var formOptionsProperties = GetProperties(typeof(FormOptions)); + var formLimitsAttributeProperties = GetProperties(typeof(RequestFormLimitsAttribute)); + + // Act & Assert + foreach (var property in formOptionsProperties) + { + var formLimiAttributeProperty = formLimitsAttributeProperties + .Where(pi => property.Name == pi.Name && pi.PropertyType == property.PropertyType) + .SingleOrDefault(); + Assert.NotNull(formLimiAttributeProperty); + } + } + + [Fact] + public void CreatesFormOptions_WithDefaults() + { + // Arrange + var formOptionsProperties = GetProperties(typeof(FormOptions)); + var formLimitsAttributeProperties = GetProperties(typeof(RequestFormLimitsAttribute)); + var formOptions = new FormOptions(); + + // Act + var requestFormLimitsAttribute = new RequestFormLimitsAttribute(); + + // Assert + foreach (var formOptionsProperty in formOptionsProperties) + { + var formLimitsAttributeProperty = formLimitsAttributeProperties + .Where(pi => pi.Name == formOptionsProperty.Name && pi.PropertyType == formOptionsProperty.PropertyType) + .SingleOrDefault(); + + Assert.Equal( + formOptionsProperty.GetValue(formOptions), + formLimitsAttributeProperty.GetValue(requestFormLimitsAttribute)); + } + } + + [Fact] + public void UpdatesFormOptions_WithOverridenValues() + { + // Arrange + var requestFormLimitsAttribute = new RequestFormLimitsAttribute(); + + // Act + requestFormLimitsAttribute.BufferBody = true; + requestFormLimitsAttribute.BufferBodyLengthLimit = 0; + requestFormLimitsAttribute.KeyLengthLimit = 0; + requestFormLimitsAttribute.MemoryBufferThreshold = 0; + requestFormLimitsAttribute.MultipartBodyLengthLimit = 0; + requestFormLimitsAttribute.MultipartBoundaryLengthLimit = 0; + requestFormLimitsAttribute.MultipartHeadersCountLimit = 0; + requestFormLimitsAttribute.MultipartHeadersLengthLimit = 0; + requestFormLimitsAttribute.ValueCountLimit = 0; + requestFormLimitsAttribute.ValueLengthLimit = 0; + + // Assert + Assert.True(requestFormLimitsAttribute.FormOptions.BufferBody); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.BufferBodyLengthLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.KeyLengthLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.MemoryBufferThreshold); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.MultipartBodyLengthLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.MultipartBoundaryLengthLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.MultipartHeadersCountLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.MultipartHeadersLengthLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.ValueCountLimit); + Assert.Equal(0, requestFormLimitsAttribute.FormOptions.ValueLengthLimit); + } + + private PropertyInfo[] GetProperties(Type type) + { + return type.GetProperties(BindingFlags.Public | BindingFlags.Instance); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestFormLimitsTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestFormLimitsTest.cs new file mode 100644 index 0000000000..e0d5beef47 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RequestFormLimitsTest.cs @@ -0,0 +1,170 @@ +// 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.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class RequestFormLimitsTest : IClassFixture> + { + public RequestFormLimitsTest(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Fact] + public async Task RequestFormLimitCheckHappens_BeforeAntiforgeryTokenValidation() + { + // Arrange + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Controller has value count limit of 2 + kvps.Add(new KeyValuePair("key1", "value1")); + kvps.Add(new KeyValuePair("key2", "value2")); + kvps.Add(new KeyValuePair("key3", "value3")); + kvps.Add(new KeyValuePair("RequestVerificationToken", "invalid-data")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/RequestFormLimitsBeforeAntiforgeryValidation", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains( + "InvalidDataException: Form value count limit 2 exceeded.", + result); + } + + [Fact] + public async Task OverridesControllerLevelLimits() + { + // Arrange + var expected = "{\"sampleInt\":10,\"sampleString\":null}"; + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Controller has a value count limit of 2, but the action has a limit of 5 + kvps.Add(new KeyValuePair("key1", "value1")); + kvps.Add(new KeyValuePair("key2", "value2")); + kvps.Add(new KeyValuePair("SampleInt", "10")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/OverrideControllerLevelLimits", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, result); + } + + [Fact] + public async Task OverrideControllerLevelLimits_UsingDefaultLimits() + { + // Arrange + var expected = "{\"sampleInt\":50,\"sampleString\":null}"; + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Controller has a key limit of 2, but the action has default limits + for (var i = 0; i < 10; i++) + { + kvps.Add(new KeyValuePair($"key{i}", $"value{i}")); + } + kvps.Add(new KeyValuePair("SampleInt", "50")); + kvps.Add(new KeyValuePair("RequestVerificationToken", "invalid-data")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/OverrideControllerLevelLimitsUsingDefaultLimits", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, result); + } + + [Fact] + public async Task RequestSizeLimitCheckHappens_BeforeRequestFormLimits() + { + // Arrange + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Request size has a limit of 100 bytes + // Request form limits has a value count limit of 2 + // Antiforgery validation is also present + kvps.Add(new KeyValuePair("key1", new string('a', 1024))); + kvps.Add(new KeyValuePair("key2", "value2")); + kvps.Add(new KeyValuePair("RequestVerificationToken", "invalid-data")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/RequestSizeLimitBeforeRequestFormLimits", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains( + "InvalidOperationException: Request content size is greater than the limit size", + result); + } + + [Fact] + public async Task RequestFormLimitsCheckHappens_AfterRequestSizeLimit() + { + // Arrange + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Request size has a limit of 100 bytes + // Request form limits has a value count limit of 2 + // Antiforgery validation is also present + kvps.Add(new KeyValuePair("key1", "value1")); + kvps.Add(new KeyValuePair("key1", "value2")); + kvps.Add(new KeyValuePair("key1", "value3")); + kvps.Add(new KeyValuePair("RequestVerificationToken", "invalid-data")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/RequestSizeLimitBeforeRequestFormLimits", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + var result = await response.Content.ReadAsStringAsync(); + Assert.Contains( + "InvalidDataException: Form value count limit 2 exceeded.", + result); + } + + [Fact] + public async Task AntiforgeryValidationHappens_AfterRequestFormAndSizeLimitCheck() + { + // Arrange + var request = new HttpRequestMessage(); + var kvps = new List>(); + // Request size has a limit of 100 bytes + // Request form limits has a value count limit of 2 + // Antiforgery validation is also present + kvps.Add(new KeyValuePair("key1", "value1")); + kvps.Add(new KeyValuePair("RequestVerificationToken", "invalid-data")); + + // Act + var response = await Client.PostAsync( + "RequestFormLimits/RequestSizeLimitBeforeRequestFormLimits", + new FormUrlEncodedContent(kvps)); + + // Assert + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + } +} diff --git a/test/WebSites/BasicWebSite/Controllers/RequestFormLimitsController.cs b/test/WebSites/BasicWebSite/Controllers/RequestFormLimitsController.cs new file mode 100644 index 0000000000..027ae44b0d --- /dev/null +++ b/test/WebSites/BasicWebSite/Controllers/RequestFormLimitsController.cs @@ -0,0 +1,59 @@ +// 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 BasicWebSite.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BasicWebSite.Controllers +{ + [RequestFormLimits(ValueCountLimit = 2)] + public class RequestFormLimitsController : Controller + { + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult RequestFormLimitsBeforeAntiforgeryValidation(Product product) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + return Json(product); + } + + [HttpPost] + [RequestFormLimits(ValueCountLimit = 5)] + public IActionResult OverrideControllerLevelLimits(Product product) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return Json(product); + } + + [HttpPost] + [RequestFormLimits] + public IActionResult OverrideControllerLevelLimitsUsingDefaultLimits(Product product) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return Json(product); + } + + [HttpPost] + [RequestFormLimits(ValueCountLimit = 2)] + [RequestSizeLimit(100)] + [ValidateAntiForgeryToken] + public IActionResult RequestSizeLimitBeforeRequestFormLimits(Product product) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + return Json(product); + } + } +}