Skip to content

Add support for options in form-binding and anti-forgery #49935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Supports configuring the behavior of form mapping in a minimal API.
/// </summary>
public class FormMappingOptionsMetadata(int? maxCollectionSize = null, int? maxRecursionDepth = null, int? maxKeySize = null)
{
/// <summary>
/// Gets or sets the maximum number of elements allowed in a form collection.
/// </summary>
public int? MaxCollectionSize { get; } = maxCollectionSize;

/// <summary>
/// Gets or sets the maximum depth allowed when recursively mapping form data.
/// </summary>
public int? MaxRecursionDepth { get; } = maxRecursionDepth;

/// <summary>
/// Gets or sets the maximum size of the buffer used to read form data keys.
/// </summary>
public int? MaxKeySize { get; } = maxKeySize;
}
76 changes: 76 additions & 0 deletions src/Http/Http.Abstractions/src/Metadata/IFormOptionsMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Http.Metadata;

/// <summary>
/// Interface marking attributes that specify limits associated with reading a form.
/// </summary>
public interface IFormOptionsMetadata
{
/// <summary>
/// Enables full request body buffering. Use this if multiple components need to read the raw stream. Defaults to false.
/// </summary>
bool? BufferBody { get; }

/// <summary>
/// If BufferBody 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. Defaults to 65,536 bytes, which is approximately 64KB.
/// </summary>
int? MemoryBufferThreshold { get; }

/// <summary>
/// If BufferBody is enabled, this is the limit for the total number of bytes that will be buffered.
/// Forms that exceed this limit will throw an InvalidDataException when parsed. Defaults to 134,217,728 bytes, which is approximately 128MB.
/// </summary>
long? BufferBodyLengthLimit { get; }

/// <summary>
/// A limit for the number of form entries to allow. Forms that exceed this limit will throw an InvalidDataException when parsed. Defaults to 1024.
/// </summary>
int? ValueCountLimit { get; }

/// <summary>
/// A limit on the length of individual keys. Forms containing keys that
/// exceed this limit will throw an InvalidDataException when parsed.
/// Defaults to 2,048 bytes, which is approximately 2KB.
/// </summary>
int? KeyLengthLimit { get; }

/// <summary>
/// A limit on the length of individual form values. Forms containing
/// values that exceed this limit will throw an InvalidDataException
/// when parsed. Defaults to 4,194,304 bytes, which is approximately 4MB.
/// </summary>
int? ValueLengthLimit { get; }

/// <summary>
/// A limit for the length of the boundary identifier. Forms with boundaries
/// that exceed this limit will throw an InvalidDataException when parsed.
/// Defaults to 128 bytes.
/// </summary>
int? MultipartBoundaryLengthLimit { get; }

/// <summary>
/// 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 InvalidDataException when parsed. Defaults to 16.
/// </summary>
int? MultipartHeadersCountLimit { get; }

/// <summary>
/// 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 InvalidDataException when parsed. Defaults to 16,384 bytes,
/// which is approximately 16KB.
/// </summary>
int? MultipartHeadersLengthLimit { get; }

/// <summary>
/// /A limit for the length of each multipart body. Forms sections that
/// exceed this limit will throw an InvalidDataException when parsed.
/// Defaults to 134,217,728 bytes, which is approximately 128MB.
/// </summary>
long? MultipartBodyLengthLimit { get; }
}
16 changes: 16 additions & 0 deletions src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! co
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.IsOptional.get -> bool
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.RequestType.get -> System.Type?
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.FormMappingOptionsMetadata(int? maxCollectionSize = null, int? maxRecursionDepth = null, int? maxKeySize = null) -> void
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxCollectionSize.get -> int?
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxKeySize.get -> int?
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxRecursionDepth.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.BufferBody.get -> bool?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.BufferBodyLengthLimit.get -> long?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.KeyLengthLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MemoryBufferThreshold.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartBodyLengthLimit.get -> long?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartBoundaryLengthLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartHeadersCountLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartHeadersLengthLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.ValueCountLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.ValueLengthLimit.get -> int?
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata.Route.get -> string!
Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.get -> System.Exception?
Expand Down
47 changes: 39 additions & 8 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ namespace Microsoft.AspNetCore.Http;
public static partial class RequestDelegateFactory
{
private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new();
private static readonly FormDataMapperOptions FormDataMapperOptions = new();

private static readonly MethodInfo ExecuteTaskWithEmptyResultMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskWithEmptyResult), BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly MethodInfo ExecuteValueTaskWithEmptyResultMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskWithEmptyResult), BindingFlags.NonPublic | BindingFlags.Static)!;
Expand Down Expand Up @@ -333,6 +332,11 @@ private static IReadOnlyList<object> AsReadOnlyList(IList<object> metadata)
// inference is skipped internally if necessary.
factoryContext.ArgumentExpressions ??= CreateArgumentsAndInferMetadata(methodInfo, factoryContext);

// Although we can re-use the cached argument expressions for most cases, parameters that are bound
// using the new form mapping logic are a special exception because we need to account for the `FormOptionsMetadata`
// added to the builder _during_ the construction of the parameter binding.
UpdateFormBindingArgumentExpressions(factoryContext);

factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, factoryContext.ArgumentExpressions);
EndpointFilterDelegate? filterPipeline = null;
var returnType = methodInfo.ReturnType;
Expand Down Expand Up @@ -753,7 +757,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
(parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!));
return useSimpleBinding
? BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext)
: BindComplexParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
: BindComplexParameterFromFormItem(parameter, string.IsNullOrEmpty(formAttribute.Name) ? parameter.Name : formAttribute.Name, factoryContext);
}
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
{
Expand Down Expand Up @@ -1982,14 +1986,40 @@ private static Expression BindParameterFromFormItem(
"form");
}

private static void UpdateFormBindingArgumentExpressions(RequestDelegateFactoryContext factoryContext)
{
if (factoryContext.ArgumentExpressions == null || factoryContext.ArgumentExpressions.Length == 0)
{
return;
}

for (var i = 0; i < factoryContext.ArgumentExpressions.Length; i++)
{
var parameter = factoryContext.Parameters[i];
var key = parameter.Name!;
if (factoryContext.TrackedParameters.TryGetValue(key, out var trackedParameter) && trackedParameter == RequestDelegateFactoryConstants.FormBindingAttribute)
{
factoryContext.ArgumentExpressions[i] = BindComplexParameterFromFormItem(parameter, key, factoryContext);
}
}
}

private static Expression BindComplexParameterFromFormItem(
ParameterInfo parameter,
string key,
RequestDelegateFactoryContext factoryContext)
{
factoryContext.FirstFormRequestBodyParameter ??= parameter;
factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
factoryContext.TrackedParameters.TryAdd(key, RequestDelegateFactoryConstants.FormBindingAttribute);
factoryContext.ReadForm = true;
var formDataMapperOptions = new FormDataMapperOptions();
var formMappingOptionsMetadatas = factoryContext.EndpointBuilder.Metadata.OfType<FormMappingOptionsMetadata>();
foreach (var formMappingOptionsMetadata in formMappingOptionsMetadatas)
{
formDataMapperOptions.MaxRecursionDepth = formMappingOptionsMetadata.MaxRecursionDepth ?? formDataMapperOptions.MaxRecursionDepth;
formDataMapperOptions.MaxCollectionSize = formMappingOptionsMetadata.MaxCollectionSize ?? formDataMapperOptions.MaxCollectionSize;
formDataMapperOptions.MaxKeyBufferSize = formMappingOptionsMetadata.MaxKeySize ?? formDataMapperOptions.MaxKeyBufferSize;
}

// var name_local;
// var name_reader;
Expand All @@ -2001,19 +2031,19 @@ private static Expression BindComplexParameterFromFormItem(
var formBuffer = Expression.Variable(typeof(char[]), "form_buffer");

// ProcessForm(context.Request.Form, form_dict, form_buffer);
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, formDict, formBuffer);
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, Expression.Constant(formDataMapperOptions.MaxKeyBufferSize), formDict, formBuffer);
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, FormDataMapperOptions.MaxKeyBufferSize));
var initializeReaderExpr = Expression.Assign(
formReader,
Expression.New(FormDataReaderConstructor,
formDict,
Expression.Constant(CultureInfo.InvariantCulture),
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(FormDataMapperOptions.MaxKeyBufferSize))));
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(formDataMapperOptions.MaxKeyBufferSize))));
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
var invokeMapMethodExpr = Expression.Call(
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
formReader,
Expression.Constant(FormDataMapperOptions));
Expression.Constant(formDataMapperOptions));
// if (form_buffer != null)
// {
// ArrayPool<char>.Shared.Return(form_buffer, false);
Expand All @@ -2039,15 +2069,15 @@ private static Expression BindComplexParameterFromFormItem(
);
}

private static void ProcessForm(IFormCollection form, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
private static void ProcessForm(IFormCollection form, int maxKeyBufferSize, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
{
var dictionary = new Dictionary<FormKey, StringValues>();
foreach (var (key, value) in form)
{
dictionary.Add(new FormKey(key.AsMemory()), value);
}
formDictionary = dictionary.AsReadOnly();
buffer = ArrayPool<char>.Shared.Rent(FormDataMapperOptions.MaxKeyBufferSize);
buffer = ArrayPool<char>.Shared.Rent(maxKeyBufferSize);
}

private static Expression BindParameterFromFormFiles(
Expand Down Expand Up @@ -2447,6 +2477,7 @@ private static class RequestDelegateFactoryConstants
public const string ServiceAttribute = "Service (Attribute)";
public const string FormFileAttribute = "Form File (Attribute)";
public const string FormAttribute = "Form (Attribute)";
public const string FormBindingAttribute = "Form Binding (Attribute)";
public const string RouteParameter = "Route (Inferred)";
public const string QueryStringParameter = "Query String (Inferred)";
public const string ServiceParameter = "Services (Inferred)";
Expand Down
Loading