Skip to content

Commit 28e5590

Browse files
authored
Add support for options in form-binding and anti-forgery (#49935)
* Add support for options in form-binding and anti-forgery * Address feedback from review
1 parent 12b48e6 commit 28e5590

23 files changed

+1272
-19
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Supports configuring the behavior of form mapping in a minimal API.
8+
/// </summary>
9+
public class FormMappingOptionsMetadata(int? maxCollectionSize = null, int? maxRecursionDepth = null, int? maxKeySize = null)
10+
{
11+
/// <summary>
12+
/// Gets or sets the maximum number of elements allowed in a form collection.
13+
/// </summary>
14+
public int? MaxCollectionSize { get; } = maxCollectionSize;
15+
16+
/// <summary>
17+
/// Gets or sets the maximum depth allowed when recursively mapping form data.
18+
/// </summary>
19+
public int? MaxRecursionDepth { get; } = maxRecursionDepth;
20+
21+
/// <summary>
22+
/// Gets or sets the maximum size of the buffer used to read form data keys.
23+
/// </summary>
24+
public int? MaxKeySize { get; } = maxKeySize;
25+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Http.Metadata;
5+
6+
/// <summary>
7+
/// Interface marking attributes that specify limits associated with reading a form.
8+
/// </summary>
9+
public interface IFormOptionsMetadata
10+
{
11+
/// <summary>
12+
/// Enables full request body buffering. Use this if multiple components need to read the raw stream. Defaults to false.
13+
/// </summary>
14+
bool? BufferBody { get; }
15+
16+
/// <summary>
17+
/// If BufferBody is enabled, this many bytes of the body will be buffered in memory.
18+
/// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead.
19+
/// This also applies when buffering individual multipart section bodies. Defaults to 65,536 bytes, which is approximately 64KB.
20+
/// </summary>
21+
int? MemoryBufferThreshold { get; }
22+
23+
/// <summary>
24+
/// If BufferBody is enabled, this is the limit for the total number of bytes that will be buffered.
25+
/// Forms that exceed this limit will throw an InvalidDataException when parsed. Defaults to 134,217,728 bytes, which is approximately 128MB.
26+
/// </summary>
27+
long? BufferBodyLengthLimit { get; }
28+
29+
/// <summary>
30+
/// A limit for the number of form entries to allow. Forms that exceed this limit will throw an InvalidDataException when parsed. Defaults to 1024.
31+
/// </summary>
32+
int? ValueCountLimit { get; }
33+
34+
/// <summary>
35+
/// A limit on the length of individual keys. Forms containing keys that
36+
/// exceed this limit will throw an InvalidDataException when parsed.
37+
/// Defaults to 2,048 bytes, which is approximately 2KB.
38+
/// </summary>
39+
int? KeyLengthLimit { get; }
40+
41+
/// <summary>
42+
/// A limit on the length of individual form values. Forms containing
43+
/// values that exceed this limit will throw an InvalidDataException
44+
/// when parsed. Defaults to 4,194,304 bytes, which is approximately 4MB.
45+
/// </summary>
46+
int? ValueLengthLimit { get; }
47+
48+
/// <summary>
49+
/// A limit for the length of the boundary identifier. Forms with boundaries
50+
/// that exceed this limit will throw an InvalidDataException when parsed.
51+
/// Defaults to 128 bytes.
52+
/// </summary>
53+
int? MultipartBoundaryLengthLimit { get; }
54+
55+
/// <summary>
56+
/// A limit for the number of headers to allow in each multipart section.
57+
/// Headers with the same name will be combined. Form sections that exceed
58+
/// this limit will throw an InvalidDataException when parsed. Defaults to 16.
59+
/// </summary>
60+
int? MultipartHeadersCountLimit { get; }
61+
62+
/// <summary>
63+
/// A limit for the total length of the header keys and values in each
64+
/// multipart section. Form sections that exceed this limit will throw
65+
/// an InvalidDataException when parsed. Defaults to 16,384 bytes,
66+
/// which is approximately 16KB.
67+
/// </summary>
68+
int? MultipartHeadersLengthLimit { get; }
69+
70+
/// <summary>
71+
/// /A limit for the length of each multipart body. Forms sections that
72+
/// exceed this limit will throw an InvalidDataException when parsed.
73+
/// Defaults to 134,217,728 bytes, which is approximately 128MB.
74+
/// </summary>
75+
long? MultipartBodyLengthLimit { get; }
76+
}

src/Http/Http.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.AcceptsMetadata(string![]! co
1313
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.ContentTypes.get -> System.Collections.Generic.IReadOnlyList<string!>!
1414
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.IsOptional.get -> bool
1515
Microsoft.AspNetCore.Http.Metadata.AcceptsMetadata.RequestType.get -> System.Type?
16+
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata
17+
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.FormMappingOptionsMetadata(int? maxCollectionSize = null, int? maxRecursionDepth = null, int? maxKeySize = null) -> void
18+
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxCollectionSize.get -> int?
19+
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxKeySize.get -> int?
20+
Microsoft.AspNetCore.Http.Metadata.FormMappingOptionsMetadata.MaxRecursionDepth.get -> int?
21+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata
22+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.BufferBody.get -> bool?
23+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.BufferBodyLengthLimit.get -> long?
24+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.KeyLengthLimit.get -> int?
25+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MemoryBufferThreshold.get -> int?
26+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartBodyLengthLimit.get -> long?
27+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartBoundaryLengthLimit.get -> int?
28+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartHeadersCountLimit.get -> int?
29+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.MultipartHeadersLengthLimit.get -> int?
30+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.ValueCountLimit.get -> int?
31+
Microsoft.AspNetCore.Http.Metadata.IFormOptionsMetadata.ValueLengthLimit.get -> int?
1632
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata
1733
Microsoft.AspNetCore.Http.Metadata.IRouteDiagnosticsMetadata.Route.get -> string!
1834
Microsoft.AspNetCore.Http.ProblemDetailsContext.Exception.get -> System.Exception?

src/Http/Http.Extensions/src/RequestDelegateFactory.cs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ namespace Microsoft.AspNetCore.Http;
3939
public static partial class RequestDelegateFactory
4040
{
4141
private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new();
42-
private static readonly FormDataMapperOptions FormDataMapperOptions = new();
4342

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

335+
// Although we can re-use the cached argument expressions for most cases, parameters that are bound
336+
// using the new form mapping logic are a special exception because we need to account for the `FormOptionsMetadata`
337+
// added to the builder _during_ the construction of the parameter binding.
338+
UpdateFormBindingArgumentExpressions(factoryContext);
339+
336340
factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, factoryContext.ArgumentExpressions);
337341
EndpointFilterDelegate? filterPipeline = null;
338342
var returnType = methodInfo.ReturnType;
@@ -753,7 +757,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
753757
(parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!));
754758
return useSimpleBinding
755759
? BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext)
756-
: BindComplexParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
760+
: BindComplexParameterFromFormItem(parameter, string.IsNullOrEmpty(formAttribute.Name) ? parameter.Name : formAttribute.Name, factoryContext);
757761
}
758762
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
759763
{
@@ -1982,14 +1986,40 @@ private static Expression BindParameterFromFormItem(
19821986
"form");
19831987
}
19841988

1989+
private static void UpdateFormBindingArgumentExpressions(RequestDelegateFactoryContext factoryContext)
1990+
{
1991+
if (factoryContext.ArgumentExpressions == null || factoryContext.ArgumentExpressions.Length == 0)
1992+
{
1993+
return;
1994+
}
1995+
1996+
for (var i = 0; i < factoryContext.ArgumentExpressions.Length; i++)
1997+
{
1998+
var parameter = factoryContext.Parameters[i];
1999+
var key = parameter.Name!;
2000+
if (factoryContext.TrackedParameters.TryGetValue(key, out var trackedParameter) && trackedParameter == RequestDelegateFactoryConstants.FormBindingAttribute)
2001+
{
2002+
factoryContext.ArgumentExpressions[i] = BindComplexParameterFromFormItem(parameter, key, factoryContext);
2003+
}
2004+
}
2005+
}
2006+
19852007
private static Expression BindComplexParameterFromFormItem(
19862008
ParameterInfo parameter,
19872009
string key,
19882010
RequestDelegateFactoryContext factoryContext)
19892011
{
19902012
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1991-
factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
2013+
factoryContext.TrackedParameters.TryAdd(key, RequestDelegateFactoryConstants.FormBindingAttribute);
19922014
factoryContext.ReadForm = true;
2015+
var formDataMapperOptions = new FormDataMapperOptions();
2016+
var formMappingOptionsMetadatas = factoryContext.EndpointBuilder.Metadata.OfType<FormMappingOptionsMetadata>();
2017+
foreach (var formMappingOptionsMetadata in formMappingOptionsMetadatas)
2018+
{
2019+
formDataMapperOptions.MaxRecursionDepth = formMappingOptionsMetadata.MaxRecursionDepth ?? formDataMapperOptions.MaxRecursionDepth;
2020+
formDataMapperOptions.MaxCollectionSize = formMappingOptionsMetadata.MaxCollectionSize ?? formDataMapperOptions.MaxCollectionSize;
2021+
formDataMapperOptions.MaxKeyBufferSize = formMappingOptionsMetadata.MaxKeySize ?? formDataMapperOptions.MaxKeyBufferSize;
2022+
}
19932023

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

20032033
// ProcessForm(context.Request.Form, form_dict, form_buffer);
2004-
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, formDict, formBuffer);
2034+
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, Expression.Constant(formDataMapperOptions.MaxKeyBufferSize), formDict, formBuffer);
20052035
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, FormDataMapperOptions.MaxKeyBufferSize));
20062036
var initializeReaderExpr = Expression.Assign(
20072037
formReader,
20082038
Expression.New(FormDataReaderConstructor,
20092039
formDict,
20102040
Expression.Constant(CultureInfo.InvariantCulture),
2011-
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(FormDataMapperOptions.MaxKeyBufferSize))));
2041+
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(formDataMapperOptions.MaxKeyBufferSize))));
20122042
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
20132043
var invokeMapMethodExpr = Expression.Call(
20142044
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
20152045
formReader,
2016-
Expression.Constant(FormDataMapperOptions));
2046+
Expression.Constant(formDataMapperOptions));
20172047
// if (form_buffer != null)
20182048
// {
20192049
// ArrayPool<char>.Shared.Return(form_buffer, false);
@@ -2039,15 +2069,15 @@ private static Expression BindComplexParameterFromFormItem(
20392069
);
20402070
}
20412071

2042-
private static void ProcessForm(IFormCollection form, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
2072+
private static void ProcessForm(IFormCollection form, int maxKeyBufferSize, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
20432073
{
20442074
var dictionary = new Dictionary<FormKey, StringValues>();
20452075
foreach (var (key, value) in form)
20462076
{
20472077
dictionary.Add(new FormKey(key.AsMemory()), value);
20482078
}
20492079
formDictionary = dictionary.AsReadOnly();
2050-
buffer = ArrayPool<char>.Shared.Rent(FormDataMapperOptions.MaxKeyBufferSize);
2080+
buffer = ArrayPool<char>.Shared.Rent(maxKeyBufferSize);
20512081
}
20522082

20532083
private static Expression BindParameterFromFormFiles(
@@ -2447,6 +2477,7 @@ private static class RequestDelegateFactoryConstants
24472477
public const string ServiceAttribute = "Service (Attribute)";
24482478
public const string FormFileAttribute = "Form File (Attribute)";
24492479
public const string FormAttribute = "Form (Attribute)";
2480+
public const string FormBindingAttribute = "Form Binding (Attribute)";
24502481
public const string RouteParameter = "Route (Inferred)";
24512482
public const string QueryStringParameter = "Query String (Inferred)";
24522483
public const string ServiceParameter = "Services (Inferred)";

0 commit comments

Comments
 (0)