Place limits on model binding collection size and recursion depth (#7214)

- #7052
- add MvcOptions.MaxModelBindingCollectionSize` and `MvcOptions.MaxModelBindingRecursionDepth`

nits:
- update syntax of a few `Resources.Designer.cs` files (I ran `/t:resx` on Mvc.sln)
- take VS suggestions in a few test classes
This commit is contained in:
Doug Bunting 2019-02-19 15:17:56 -08:00 committed by GitHub
parent 69abefa06f
commit 3e0c75187c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 836 additions and 114 deletions

View File

@ -15,64 +15,56 @@ namespace Microsoft.AspNetCore.Hosting
/// </summary>
internal static string ErrorPageHtml_Title
{
get { return GetString("ErrorPageHtml_Title"); }
get => GetString("ErrorPageHtml_Title");
}
/// <summary>
/// Internal Server Error
/// </summary>
internal static string FormatErrorPageHtml_Title()
{
return GetString("ErrorPageHtml_Title");
}
=> GetString("ErrorPageHtml_Title");
/// <summary>
/// An error occurred while starting the application.
/// </summary>
internal static string ErrorPageHtml_UnhandledException
{
get { return GetString("ErrorPageHtml_UnhandledException"); }
get => GetString("ErrorPageHtml_UnhandledException");
}
/// <summary>
/// An error occurred while starting the application.
/// </summary>
internal static string FormatErrorPageHtml_UnhandledException()
{
return GetString("ErrorPageHtml_UnhandledException");
}
=> GetString("ErrorPageHtml_UnhandledException");
/// <summary>
/// Unknown location
/// </summary>
internal static string ErrorPageHtml_UnknownLocation
{
get { return GetString("ErrorPageHtml_UnknownLocation"); }
get => GetString("ErrorPageHtml_UnknownLocation");
}
/// <summary>
/// Unknown location
/// </summary>
internal static string FormatErrorPageHtml_UnknownLocation()
{
return GetString("ErrorPageHtml_UnknownLocation");
}
=> GetString("ErrorPageHtml_UnknownLocation");
/// <summary>
/// WebHostBuilder allows creation only of a single instance of WebHost
/// </summary>
internal static string WebHostBuilder_SingleInstance
{
get { return GetString("WebHostBuilder_SingleInstance"); }
get => GetString("WebHostBuilder_SingleInstance");
}
/// <summary>
/// WebHostBuilder allows creation only of a single instance of WebHost
/// </summary>
internal static string FormatWebHostBuilder_SingleInstance()
{
return GetString("WebHostBuilder_SingleInstance");
}
=> GetString("WebHostBuilder_SingleInstance");
private static string GetString(string name, params string[] formatterNames)
{

View File

@ -1,6 +1,7 @@
// <auto-generated />
namespace Microsoft.AspNetCore.Localization
{
using System.Globalization;
using System.Reflection;
using System.Resources;
@ -14,9 +15,15 @@ namespace Microsoft.AspNetCore.Localization
/// </summary>
internal static string Exception_CulturesShouldNotBeEmpty
{
get { return GetString("Exception_CulturesShouldNotBeEmpty"); }
get => GetString("Exception_CulturesShouldNotBeEmpty");
}
/// <summary>
/// Please provide at least one culture.
/// </summary>
internal static string FormatException_CulturesShouldNotBeEmpty()
=> GetString("Exception_CulturesShouldNotBeEmpty");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -15,96 +15,84 @@ namespace Microsoft.AspNetCore.Session
/// </summary>
internal static string Exception_KeyLengthIsExceeded
{
get { return GetString("Exception_KeyLengthIsExceeded"); }
get => GetString("Exception_KeyLengthIsExceeded");
}
/// <summary>
/// The key cannot be longer than '{0}' when encoded with UTF-8.
/// </summary>
internal static string FormatException_KeyLengthIsExceeded(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Exception_KeyLengthIsExceeded"), p0);
}
=> string.Format(CultureInfo.CurrentCulture, GetString("Exception_KeyLengthIsExceeded"), p0);
/// <summary>
/// The session cannot be established after the response has started.
/// </summary>
internal static string Exception_InvalidSessionEstablishment
{
get { return GetString("Exception_InvalidSessionEstablishment"); }
get => GetString("Exception_InvalidSessionEstablishment");
}
/// <summary>
/// The session cannot be established after the response has started.
/// </summary>
internal static string FormatException_InvalidSessionEstablishment()
{
return GetString("Exception_InvalidSessionEstablishment");
}
=> GetString("Exception_InvalidSessionEstablishment");
/// <summary>
/// The value cannot be serialized in two bytes.
/// </summary>
internal static string Exception_InvalidToSerializeIn2Bytes
{
get { return GetString("Exception_InvalidToSerializeIn2Bytes"); }
get => GetString("Exception_InvalidToSerializeIn2Bytes");
}
/// <summary>
/// The value cannot be serialized in two bytes.
/// </summary>
internal static string FormatException_InvalidToSerializeIn2Bytes()
{
return GetString("Exception_InvalidToSerializeIn2Bytes");
}
=> GetString("Exception_InvalidToSerializeIn2Bytes");
/// <summary>
/// The value cannot be serialized in three bytes.
/// </summary>
internal static string Exception_InvalidToSerializeIn3Bytes
{
get { return GetString("Exception_InvalidToSerializeIn3Bytes"); }
get => GetString("Exception_InvalidToSerializeIn3Bytes");
}
/// <summary>
/// The value cannot be serialized in three bytes.
/// </summary>
internal static string FormatException_InvalidToSerializeIn3Bytes()
{
return GetString("Exception_InvalidToSerializeIn3Bytes");
}
=> GetString("Exception_InvalidToSerializeIn3Bytes");
/// <summary>
/// The value cannot be negative.
/// </summary>
internal static string Exception_NumberShouldNotBeNegative
{
get { return GetString("Exception_NumberShouldNotBeNegative"); }
get => GetString("Exception_NumberShouldNotBeNegative");
}
/// <summary>
/// The value cannot be negative.
/// </summary>
internal static string FormatException_NumberShouldNotBeNegative()
{
return GetString("Exception_NumberShouldNotBeNegative");
}
=> GetString("Exception_NumberShouldNotBeNegative");
/// <summary>
/// Argument cannot be null or empty string.
/// </summary>
internal static string ArgumentCannotBeNullOrEmpty
{
get { return GetString("ArgumentCannotBeNullOrEmpty"); }
get => GetString("ArgumentCannotBeNullOrEmpty");
}
/// <summary>
/// Argument cannot be null or empty string.
/// </summary>
internal static string FormatArgumentCannotBeNullOrEmpty()
{
return GetString("ArgumentCannotBeNullOrEmpty");
}
=> GetString("ArgumentCannotBeNullOrEmpty");
private static string GetString(string name, params string[] formatterNames)
{

View File

@ -191,8 +191,6 @@ namespace Microsoft.AspNetCore.Mvc.Description
var action = CreateActionDescriptor(nameof(FromRouting));
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = action.Parameters[0];
// Act
var descriptions = GetApiDescriptions(action);
@ -1511,7 +1509,6 @@ namespace Microsoft.AspNetCore.Mvc.Description
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsHasCollection));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
@ -1531,7 +1528,6 @@ namespace Microsoft.AspNetCore.Mvc.Description
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsHasCollection_Complex));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);

View File

@ -52,6 +52,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
}
/// <summary>
/// Creates a new <see cref="ArrayModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
/// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
/// <remarks>
/// <para>This is the preferred <see cref="ArrayModelBinder{TElement}"/> constructor.</para>
/// <para>
/// The <paramref name="allowValidatingTopLevelNodes"/> parameter is currently ignored.
/// <see cref="CollectionModelBinder{TElement}.AllowValidatingTopLevelNodes"/> is always <see langword="true"/>
/// in <see cref="ArrayModelBinder{TElement}"/>.
/// </para>
/// </remarks>
public ArrayModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes,
MvcOptions mvcOptions)
: base(elementBinder, loggerFactory, allowValidatingTopLevelNodes: true, mvcOptions)
{
}
/// <inheritdoc />
public override bool CanCreateInstance(Type targetType)
{

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
@ -23,15 +24,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (context.Metadata.ModelType.IsArray)
{
var elementType = context.Metadata.ElementMetadata.ModelType;
var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType);
var elementBinder = context.CreateBinder(context.Metadata.ElementMetadata);
var binderType = typeof(ArrayModelBinder<>).MakeGenericType(elementType);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
true /* allowValidatingTopLevelNodes */);
true /* allowValidatingTopLevelNodes */,
mvcOptions);
}
return null;

View File

@ -9,6 +9,8 @@ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
@ -21,12 +23,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public class CollectionModelBinder<TElement> : ICollectionModelBinder
{
private static readonly IValueProvider EmptyValueProvider = new CompositeValueProvider();
private readonly int _maxModelBindingCollectionSize = MvcOptions.DefaultMaxModelBindingCollectionSize;
private Func<object> _modelCreator;
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public CollectionModelBinder(IModelBinder elementBinder, ILoggerFactory loggerFactory)
: this(elementBinder, loggerFactory, allowValidatingTopLevelNodes: true)
@ -36,7 +41,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">The <see cref="IModelBinder"/> for binding elements.</param>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
@ -63,6 +70,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
AllowValidatingTopLevelNodes = allowValidatingTopLevelNodes;
}
/// <summary>
/// Creates a new <see cref="CollectionModelBinder{TElement}"/>.
/// </summary>
/// <param name="elementBinder">
/// The <see cref="IModelBinder"/> for binding <typeparamref name="TElement"/>.
/// </param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
/// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
/// <remarks>This is the preferred <see cref="CollectionModelBinder{TElement}"/> constructor.</remarks>
public CollectionModelBinder(
IModelBinder elementBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes,
MvcOptions mvcOptions)
: this(elementBinder, loggerFactory, allowValidatingTopLevelNodes)
{
if (mvcOptions == null)
{
throw new ArgumentNullException(nameof(mvcOptions));
}
_maxModelBindingCollectionSize = mvcOptions.MaxModelBindingCollectionSize;
}
// Internal for testing.
internal bool AllowValidatingTopLevelNodes { get; }
@ -305,8 +341,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
else
{
indexNamesIsFinite = false;
indexNames = Enumerable.Range(0, int.MaxValue)
.Select(i => i.ToString(CultureInfo.InvariantCulture));
var limit = _maxModelBindingCollectionSize == int.MaxValue ?
int.MaxValue :
_maxModelBindingCollectionSize + 1;
indexNames = Enumerable
.Range(0, limit)
.Select(i => i.ToString(CultureInfo.InvariantCulture));
}
var elementMetadata = bindingContext.ModelMetadata.ElementMetadata;
@ -345,6 +385,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
boundCollection.Add(ModelBindingHelper.CastOrDefault<TElement>(boundValue));
}
// Did the collection grow larger than the limit?
if (boundCollection.Count > _maxModelBindingCollectionSize)
{
// Look for a non-empty name. Both ModelName and OriginalModelName may be empty at the top level.
var name = string.IsNullOrEmpty(bindingContext.ModelName) ?
(string.IsNullOrEmpty(bindingContext.OriginalModelName) &&
bindingContext.ModelMetadata.MetadataKind != ModelMetadataKind.Type ?
bindingContext.ModelMetadata.Name :
bindingContext.OriginalModelName) : // This name may unfortunately be empty.
bindingContext.ModelName;
throw new InvalidOperationException(Resources.FormatModelBinding_ExceededMaxModelBindingCollectionSize(
name,
nameof(MvcOptions),
nameof(MvcOptions.MaxModelBindingCollectionSize),
_maxModelBindingCollectionSize,
bindingContext.ModelMetadata.ElementType));
}
return new CollectionResult
{
Model = boundCollection,

View File

@ -32,22 +32,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
return null;
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
// If the model type is ICollection<> then we can call its Add method, so we can always support it.
var collectionType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(ICollection<>));
if (collectionType != null)
{
var elementType = collectionType.GenericTypeArguments[0];
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
true /* allowValidatingTopLevelNodes */);
return CreateInstance(context, collectionType);
}
// If the model type is IEnumerable<> then we need to know if we can assign a List<> to it, since
@ -59,19 +48,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var listType = typeof(List<>).MakeGenericType(enumerableType.GenericTypeArguments);
if (modelType.GetTypeInfo().IsAssignableFrom(listType.GetTypeInfo()))
{
var elementType = enumerableType.GenericTypeArguments[0];
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(enumerableType.GenericTypeArguments);
return (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
true /* allowValidatingTopLevelNodes */);
return CreateInstance(context, listType);
}
}
return null;
}
private static IModelBinder CreateInstance(ModelBinderProviderContext context, Type collectionType)
{
var binderType = typeof(CollectionModelBinder<>).MakeGenericType(collectionType.GenericTypeArguments);
var elementType = collectionType.GenericTypeArguments[0];
var elementBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(elementType));
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
var binder = (IModelBinder)Activator.CreateInstance(
binderType,
elementBinder,
loggerFactory,
true /* allowValidatingTopLevelNodes */,
mvcOptions);
return binder;
}
}
}

View File

@ -74,6 +74,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
_valueBinder = valueBinder;
}
/// <summary>
/// Creates a new <see cref="DictionaryModelBinder{TKey, TValue}"/>.
/// </summary>
/// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
/// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="allowValidatingTopLevelNodes">
/// Indication that validation of top-level models is enabled. If <see langword="true"/> and
/// <see cref="ModelMetadata.IsBindingRequired"/> is <see langword="true"/> for a top-level model, the binder
/// adds a <see cref="ModelStateDictionary"/> error when the model is not bound.
/// </param>
/// <param name="mvcOptions">The <see cref="MvcOptions"/>.</param>
/// <remarks>
/// <para>This is the preferred <see cref="DictionaryModelBinder{TKey, TValue}"/> constructor.</para>
/// <para>
/// The <paramref name="allowValidatingTopLevelNodes"/> parameter is currently ignored.
/// <see cref="CollectionModelBinder{TElement}.AllowValidatingTopLevelNodes"/> is always
/// <see langword="false"/> in <see cref="DictionaryModelBinder{TKey, TValue}"/>. This class ignores that
/// property and unconditionally checks for unbound top-level models with
/// <see cref="ModelMetadata.IsBindingRequired"/>.
/// </para>
/// </remarks>
public DictionaryModelBinder(
IModelBinder keyBinder,
IModelBinder valueBinder,
ILoggerFactory loggerFactory,
bool allowValidatingTopLevelNodes,
MvcOptions mvcOptions)
: base(
new KeyValuePairModelBinder<TKey, TValue>(keyBinder, valueBinder, loggerFactory),
loggerFactory,
allowValidatingTopLevelNodes: false,
mvcOptions)
{
if (valueBinder == null)
{
throw new ArgumentNullException(nameof(valueBinder));
}
_valueBinder = valueBinder;
}
/// <inheritdoc />
public override async Task BindModelAsync(ModelBindingContext bindingContext)
{

View File

@ -27,13 +27,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
var dictionaryType = ClosedGenericMatcher.ExtractGenericInterface(modelType, typeof(IDictionary<,>));
if (dictionaryType != null)
{
var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments);
var keyType = dictionaryType.GenericTypeArguments[0];
var keyBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(keyType));
var valueType = dictionaryType.GenericTypeArguments[1];
var valueBinder = context.CreateBinder(context.MetadataProvider.GetMetadataForType(valueType));
var binderType = typeof(DictionaryModelBinder<,>).MakeGenericType(dictionaryType.GenericTypeArguments);
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var mvcOptions = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
return (IModelBinder)Activator.CreateInstance(
@ -41,7 +42,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
keyBinder,
valueBinder,
loggerFactory,
true /* allowValidatingTopLevelNodes */);
true /* allowValidatingTopLevelNodes */,
mvcOptions);
}
return null;

View File

@ -3,7 +3,10 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
@ -18,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
private ActionContext _actionContext;
private ModelStateDictionary _modelState;
private ValidationStateDictionary _validationState;
private int? _maxModelBindingRecursionDepth;
private State _state;
private readonly Stack<State> _stack = new Stack<State>();
@ -184,6 +188,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
}
private int MaxModelBindingRecursionDepth
{
get
{
if (!_maxModelBindingRecursionDepth.HasValue)
{
// Ignore incomplete initialization. This must be a test scenario because CreateBindingContext(...)
// has not been called or was called without MvcOptions in the service provider.
_maxModelBindingRecursionDepth = MvcOptions.DefaultMaxModelBindingRecursionDepth;
}
return _maxModelBindingRecursionDepth.Value;
}
set
{
_maxModelBindingRecursionDepth = value;
}
}
/// <summary>
/// Creates a new <see cref="DefaultModelBindingContext"/> for top-level model binding operation.
/// </summary>
@ -223,16 +246,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
var binderModelName = bindingInfo?.BinderModelName ?? metadata.BinderModelName;
var bindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource;
var propertyFilterProvider = bindingInfo?.PropertyFilterProvider ?? metadata.PropertyFilterProvider;
var bindingSource = bindingInfo?.BindingSource ?? metadata.BindingSource;
return new DefaultModelBindingContext()
var bindingContext = new DefaultModelBindingContext()
{
ActionContext = actionContext,
BinderModelName = binderModelName,
BindingSource = bindingSource,
PropertyFilter = propertyFilterProvider?.PropertyFilter,
ValidationState = new ValidationStateDictionary(),
// Because this is the top-level context, FieldName and ModelName should be the same.
FieldName = binderModelName ?? modelName,
@ -245,9 +268,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
OriginalValueProvider = valueProvider,
ValueProvider = FilterValueProvider(valueProvider, bindingSource),
ValidationState = new ValidationStateDictionary(),
};
// mvcOptions may be null when this method is called in test scenarios.
var mvcOptions = actionContext.HttpContext.RequestServices?.GetService<IOptions<MvcOptions>>();
if (mvcOptions != null)
{
bindingContext.MaxModelBindingRecursionDepth = mvcOptions.Value.MaxModelBindingRecursionDepth;
}
return bindingContext;
}
/// <inheritdoc />
@ -299,6 +329,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
_stack.Push(_state);
// Would this new scope (which isn't in _stack) exceed the allowed recursion depth? That is, has the model
// binding system already nested MaxModelBindingRecursionDepth binders?
if (_stack.Count >= MaxModelBindingRecursionDepth)
{
// Find the root of this deeply-nested model.
var states = _stack.ToArray();
var rootModelType = states[states.Length - 1].ModelMetadata.ModelType;
throw new InvalidOperationException(Resources.FormatModelBinding_ExceededMaxModelBindingRecursionDepth(
nameof(MvcOptions),
nameof(MvcOptions.MaxModelBindingRecursionDepth),
MaxModelBindingRecursionDepth,
rootModelType));
}
Result = default;
return new NestedScope(this);

View File

@ -4,15 +4,16 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.WebUtilities;
namespace Microsoft.AspNetCore.Mvc
{
@ -21,8 +22,14 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary>
public class MvcOptions : IEnumerable<ICompatibilitySwitch>
{
internal const int DefaultMaxModelBindingCollectionSize = FormReader.DefaultValueCountLimit;
internal const int DefaultMaxModelBindingRecursionDepth = 32;
private readonly IReadOnlyList<ICompatibilitySwitch> _switches = Array.Empty<ICompatibilitySwitch>();
private int _maxModelStateErrors = ModelStateDictionary.DefaultMaxAllowedErrors;
private int _maxModelBindingCollectionSize = DefaultMaxModelBindingCollectionSize;
private int _maxModelBindingRecursionDepth = DefaultMaxModelBindingRecursionDepth;
private int? _maxValidationDepth = 32;
/// <summary>
@ -231,6 +238,85 @@ namespace Microsoft.AspNetCore.Mvc
/// </value>
public bool SuppressAsyncSuffixInActionNames { get; set; } = true;
/// <summary>
/// Gets or sets the maximum size of a complex collection to model bind. When this limit is reached, the model
/// binding system will throw an <see cref="InvalidOperationException"/>.
/// </summary>
/// <remarks>
/// <para>
/// When binding a collection, some element binders may succeed unconditionally and model binding may run out
/// of memory. This limit constrains such unbounded collection growth; it is a safeguard against incorrect
/// model binders and models.
/// </para>
/// <para>
/// This limit does not <em>correct</em> the bound model. The <see cref="InvalidOperationException"/> instead
/// informs the developer of an issue in their model or model binder. The developer must correct that issue.
/// </para>
/// <para>
/// This limit does not apply to collections of simple types. When
/// <see cref="CollectionModelBinder{TElement}"/> relies entirely on <see cref="IValueProvider"/>s, it cannot
/// create collections larger than the available data.
/// </para>
/// <para>
/// A very high value for this option (<c>int.MaxValue</c> for example) effectively removes the limit and is
/// not recommended.
/// </para>
/// </remarks>
/// <value>The default value is <c>1024</c>, matching <see cref="FormReader.DefaultValueCountLimit"/>.</value>
public int MaxModelBindingCollectionSize
{
get => _maxModelBindingCollectionSize;
set
{
// Disallowing an empty collection would cause the CollectionModelBinder to throw unconditionally.
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_maxModelBindingCollectionSize = value;
}
}
/// <summary>
/// Gets or sets the maximum recursion depth of the model binding system. The
/// <see cref="DefaultModelBindingContext"/> will throw an <see cref="InvalidOperationException"/> if more than
/// this number of <see cref="IModelBinder"/>s are on the stack. That is, an attempt to recurse beyond this
/// level will fail.
/// </summary>
/// <remarks>
/// <para>
/// For some self-referential models, some binders may succeed unconditionally and model binding may result in
/// stack overflow. This limit constrains such unbounded recursion; it is a safeguard against incorrect model
/// binders and models. This limit also protects against very deep model type hierarchies lacking
/// self-references.
/// </para>
/// <para>
/// This limit does not <em>correct</em> the bound model. The <see cref="InvalidOperationException"/> instead
/// informs the developer of an issue in their model. The developer must correct that issue.
/// </para>
/// <para>
/// A very high value for this option (<c>int.MaxValue</c> for example) effectively removes the limit and is
/// not recommended.
/// </para>
/// </remarks>
/// <value>The default value is <c>32</c>, matching the default <see cref="MaxValidationDepth"/> value.</value>
public int MaxModelBindingRecursionDepth
{
get => _maxModelBindingRecursionDepth;
set
{
// Disallowing one model binder (if supported) would cause the model binding system to throw
// unconditionally. DefaultModelBindingContext always allows a top-level binder i.e. its own creation.
if (value <= 1)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
_maxModelBindingRecursionDepth = value;
}
}
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() => _switches.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();

View File

@ -1704,6 +1704,34 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatReferenceToNewtonsoftJsonRequired(object p0, object p1, object p2, object p3, object p4)
=> string.Format(CultureInfo.CurrentCulture, GetString("ReferenceToNewtonsoftJsonRequired"), p0, p1, p2, p3, p4);
/// <summary>
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
/// </summary>
internal static string ModelBinding_ExceededMaxModelBindingCollectionSize
{
get => GetString("ModelBinding_ExceededMaxModelBindingCollectionSize");
}
/// <summary>
/// Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
/// </summary>
internal static string FormatModelBinding_ExceededMaxModelBindingCollectionSize(object p0, object p1, object p2, object p3, object p4)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_ExceededMaxModelBindingCollectionSize"), p0, p1, p2, p3, p4);
/// <summary>
/// Model binding system exceeded {0}.{1} ({2}). Reduce the potential nesting of '{3}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
/// </summary>
internal static string ModelBinding_ExceededMaxModelBindingRecursionDepth
{
get => GetString("ModelBinding_ExceededMaxModelBindingRecursionDepth");
}
/// <summary>
/// Model binding system exceeded {0}.{1} ({2}). Reduce the potential nesting of '{3}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.
/// </summary>
internal static string FormatModelBinding_ExceededMaxModelBindingRecursionDepth(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelBinding_ExceededMaxModelBindingRecursionDepth"), p0, p1, p2, p3);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -493,4 +493,12 @@
<data name="ReferenceToNewtonsoftJsonRequired" xml:space="preserve">
<value>'{0}' requires a reference to '{1}'. Configure your application by adding a reference to the '{1}' package and calling '{2}.{3}' inside the call to '{4}' in the application startup code.</value>
</data>
<data name="ModelBinding_ExceededMaxModelBindingCollectionSize" xml:space="preserve">
<value>Collection bound to '{0}' exceeded {1}.{2} ({3}). This limit is a safeguard against incorrect model binders and models. Address issues in '{4}'. For example, this type may have a property with a model binder that always succeeds. See the {1}.{2} documentation for more information.</value>
<comment>{0} is model name, {1} is MvcOptions, {2} is MaxModelBindingCollectionSize, {3} is option value, {4} is affected type.</comment>
</data>
<data name="ModelBinding_ExceededMaxModelBindingRecursionDepth" xml:space="preserve">
<value>Model binding system exceeded {0}.{1} ({2}). Reduce the potential nesting of '{3}'. For example, this type may have a property with a model binder that always succeeds. See the {0}.{1} documentation for more information.</value>
<comment>{0} is MvcOptions, {1} is MaxModelBindingRecursionDepth, {2} is option value, {3} is (loopy or deeply nested) top-level model type.</comment>
</data>
</root>

View File

@ -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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -369,5 +370,38 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
(e) => Assert.Equal("Alias1", e),
(e) => Assert.Equal("Alias2", e));
}
[Fact]
public async Task ArrayModelBinder_ThrowsOn1025Items_AtTopLevel()
{
// Arrange
var expectedMessage = $"Collection bound to 'parameter' exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} (1024). This limit is a " +
$"safeguard against incorrect model binders and models. Address issues in " +
$"'{typeof(SuccessfulModel)}'. For example, this type may have a property with a model binder that " +
$"always succeeds. See the {nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} " +
$"documentation for more information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(SuccessfulModel[]),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
// CollectionModelBinder binds an empty collection when value providers are all empty.
request.QueryString = new QueryString("?a=b");
});
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
}
}
}

View File

@ -968,6 +968,164 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(0, modelState.ErrorCount);
}
// Regression test for #7052
[Fact]
public async Task CollectionModelBinder_ThrowsOn1025Items_AtTopLevel()
{
// Arrange
var expectedMessage = $"Collection bound to 'parameter' exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} (1024). This limit is a " +
$"safeguard against incorrect model binders and models. Address issues in " +
$"'{typeof(SuccessfulModel)}'. For example, this type may have a property with a model binder that " +
$"always succeeds. See the {nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} " +
$"documentation for more information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(IList<SuccessfulModel>),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
// CollectionModelBinder binds an empty collection when value providers are all empty.
request.QueryString = new QueryString("?a=b");
});
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
// Ensure CollectionModelBinder allows MaxModelBindingCollectionSize items.
[Fact]
public async Task CollectionModelBinder_Binds3Items_WithIndices()
{
// Arrange
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(IList<SuccessfulModel>),
};
var testContext = ModelBindingTestHelper.GetTestContext(
request =>
{
request.QueryString = new QueryString("?Index=0&Index=1&Index=2");
},
options => options.MaxModelBindingCollectionSize = 3);
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelState.IsValid);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(result.IsModelSet);
var locations = Assert.IsType<List<SuccessfulModel>>(result.Model);
Assert.Collection(
locations,
item =>
{
Assert.True(item.IsBound);
Assert.Null(item.Name);
},
item =>
{
Assert.True(item.IsBound);
Assert.Null(item.Name);
},
item =>
{
Assert.True(item.IsBound);
Assert.Null(item.Name);
});
}
// Ensure CollectionModelBinder disallows one more than MaxModelBindingCollectionSize items.
[Fact]
public async Task CollectionModelBinder_ThrowsOn4Items_WithIndices()
{
// Arrange
var expectedMessage = $"Collection bound to 'parameter' exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} (3). This limit is a " +
$"safeguard against incorrect model binders and models. Address issues in " +
$"'{typeof(SuccessfulModel)}'. For example, this type may have a property with a model binder that " +
$"always succeeds. See the {nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} " +
$"documentation for more information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(IList<SuccessfulModel>),
};
var testContext = ModelBindingTestHelper.GetTestContext(
request =>
{
request.QueryString = new QueryString("?Index=0&Index=1&Index=2&Index=3");
},
options => options.MaxModelBindingCollectionSize = 3);
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private class SuccessfulContainer
{
public IList<SuccessfulModel> Successes { get; set; }
}
[Fact]
public async Task CollectionModelBinder_ThrowsOn1025Items()
{
// Arrange
var expectedMessage = $"Collection bound to 'Successes' exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} (1024). This limit is a " +
$"safeguard against incorrect model binders and models. Address issues in " +
$"'{typeof(SuccessfulModel)}'. For example, this type may have a property with a model binder that " +
$"always succeeds. See the {nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} " +
$"documentation for more information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(SuccessfulContainer),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
// CollectionModelBinder binds an empty collection when value providers lack matching data.
request.QueryString = new QueryString("?Successes[0]=b");
});
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private class ClosedGenericCollection : Collection<string>
{
}
@ -978,7 +1136,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitClosedGenericCollection : ICollection<string>
{
private List<string> _data = new List<string>();
private readonly List<string> _data = new List<string>();
int ICollection<string>.Count
{
@ -1034,7 +1192,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitClosedGenericList : IList<string>
{
private List<string> _data = new List<string>();
private readonly List<string> _data = new List<string>();
string IList<string>.this[int index]
{
@ -1118,7 +1276,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitCollection<T> : ICollection<T>
{
private List<T> _data = new List<T>();
private readonly List<T> _data = new List<T>();
int ICollection<T>.Count
{
@ -1174,7 +1332,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitList<T> : IList<T>
{
private List<T> _data = new List<T>();
private readonly List<T> _data = new List<T>();
T IList<T>.this[int index]
{

View File

@ -3411,6 +3411,181 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.True(modelState.IsValid);
}
private class LoopyModel
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public LoopyModel SelfReference { get; set; }
}
// Regression test for #7052
[Fact]
public async Task ModelBindingSystem_ThrowsOn33Binders()
{
// Arrange
var expectedMessage = $"Model binding system exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
$"potential nesting of '{typeof(LoopyModel)}'. For example, this type may have a property with a " +
$"model binder that always succeeds. See the " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
$"information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(LoopyModel),
};
var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private class TwoDeepModel
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
}
private class ThreeDeepModel
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public TwoDeepModel Inner { get; set; }
}
// Ensure model binding system allows MaxModelBindingRecursionDepth binders on the stack.
[Fact]
public async Task ModelBindingSystem_BindsWith3Binders()
{
// Arrange
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(ThreeDeepModel),
};
var testContext = ModelBindingTestHelper.GetTestContext(
updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act
var result = await parameterBinder.BindModelAsync(parameter, testContext);
// Assert
Assert.True(modelState.IsValid);
Assert.Equal(0, modelState.ErrorCount);
Assert.True(result.IsModelSet);
var model = Assert.IsType<ThreeDeepModel>(result.Model);
Assert.True(model.IsBound);
Assert.NotNull(model.Inner);
Assert.True(model.Inner.IsBound);
}
private class FourDeepModel
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public ThreeDeepModel Inner { get; set; }
}
// Ensure model binding system disallows one more than MaxModelBindingRecursionDepth binders on the stack.
[Fact]
public async Task ModelBindingSystem_ThrowsOn4Binders()
{
// Arrange
var expectedMessage = $"Model binding system exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (3). Reduce the " +
$"potential nesting of '{typeof(FourDeepModel)}'. For example, this type may have a property with a " +
$"model binder that always succeeds. See the " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
$"information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(FourDeepModel),
};
var testContext = ModelBindingTestHelper.GetTestContext(
updateOptions: options => options.MaxModelBindingRecursionDepth = 3);
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private class LoopyModel1
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public LoopyModel2 Inner { get; set; }
}
private class LoopyModel2
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public LoopyModel3 Inner { get; set; }
}
private class LoopyModel3
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public LoopyModel1 Inner { get; set; }
}
[Fact]
public async Task ModelBindingSystem_ThrowsOn33Binders_WithIndirectModelTypeLoop()
{
// Arrange
var expectedMessage = $"Model binding system exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} (32). Reduce the " +
$"potential nesting of '{typeof(LoopyModel1)}'. For example, this type may have a property with a " +
$"model binder that always succeeds. See the " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingRecursionDepth)} documentation for more " +
$"information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(LoopyModel1),
};
var testContext = ModelBindingTestHelper.GetTestContext();
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private static void SetJsonBodyContent(HttpRequest request, string content)
{
var stream = new MemoryStream(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(content));

View File

@ -1128,6 +1128,40 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal(0, modelState.ErrorCount);
}
[Fact]
public async Task DictionaryModelBinder_ThrowsOn1025Items_AtTopLevel()
{
// Arrange
var expectedMessage = $"Collection bound to 'parameter' exceeded " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} (1024). This limit is a " +
$"safeguard against incorrect model binders and models. Address issues in " +
$"'{typeof(KeyValuePair<SuccessfulModel, SuccessfulModel>)}'. For example, this type may have a " +
$"property with a model binder that always succeeds. See the " +
$"{nameof(MvcOptions)}.{nameof(MvcOptions.MaxModelBindingCollectionSize)} documentation for more " +
$"information.";
var parameter = new ParameterDescriptor()
{
Name = "parameter",
ParameterType = typeof(Dictionary<SuccessfulModel, SuccessfulModel>),
};
var testContext = ModelBindingTestHelper.GetTestContext(request =>
{
// CollectionModelBinder binds an empty collection when value providers are all empty.
request.QueryString = new QueryString("?a=b");
});
var modelState = testContext.ModelState;
var metadata = testContext.MetadataProvider.GetMetadataForType(parameter.ParameterType);
var valueProvider = await CompositeValueProvider.CreateAsync(testContext);
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(testContext);
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => parameterBinder.BindModelAsync(parameter, testContext));
Assert.Equal(expectedMessage, exception.Message);
}
private class ClosedGenericDictionary : Dictionary<string, string>
{
}
@ -1138,7 +1172,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitClosedGenericDictionary : IDictionary<string, string>
{
private IDictionary<string, string> _data = new Dictionary<string, string>();
private readonly IDictionary<string, string> _data = new Dictionary<string, string>();
string IDictionary<string, string>.this[string key]
{
@ -1243,7 +1277,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
private class ExplicitDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
private IDictionary<TKey, TValue> _data = new Dictionary<TKey, TValue>();
private readonly IDictionary<TKey, TValue> _data = new Dictionary<TKey, TValue>();
TValue IDictionary<TKey, TValue>.this[TKey key]
{

View File

@ -0,0 +1,13 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
{
public class SuccessfulModel
{
[ModelBinder(typeof(SuccessfulModelBinder))]
public bool IsBound { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,22 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc.IntegrationTests
{
/// <summary>
/// An unconditionally-successful model binder.
/// </summary>
public class SuccessfulModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = bindingContext.ModelType == typeof(bool) ? (object)true : null;
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
}

View File

@ -2239,6 +2239,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core
/// </summary>
internal static string FormatServerShutdownDuringConnectionInitialization()
=> GetString("ServerShutdownDuringConnectionInitialization");
/// <summary>
/// Cannot call GetMemory() until response has started. Call HttpResponse.StartAsync() before calling GetMemory().
/// </summary>