Skip to content

Commit 2bcb2f6

Browse files
committed
Add support for options in form-binding and anti-forgery
1 parent cf12d96 commit 2bcb2f6

18 files changed

+1224
-9
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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ private static IReadOnlyList<object> AsReadOnlyList(IList<object> metadata)
333333
// inference is skipped internally if necessary.
334334
factoryContext.ArgumentExpressions ??= CreateArgumentsAndInferMetadata(methodInfo, factoryContext);
335335

336+
// Although we can re-use the cached argument expressions for most cases, parameters that are bound
337+
// using the new form mapping logic are a special exception because we need to account for the `FormOptionsMetadata`
338+
// added to the builder _during_ the construction of the parameter binding.
339+
UpdateFormBindingArgumentExpressions(factoryContext);
340+
336341
factoryContext.MethodCall = CreateMethodCall(methodInfo, targetExpression, factoryContext.ArgumentExpressions);
337342
EndpointFilterDelegate? filterPipeline = null;
338343
var returnType = methodInfo.ReturnType;
@@ -753,7 +758,7 @@ private static Expression CreateArgument(ParameterInfo parameter, RequestDelegat
753758
(parameter.ParameterType.IsArray && ParameterBindingMethodCache.HasTryParseMethod(parameter.ParameterType.GetElementType()!));
754759
return useSimpleBinding
755760
? BindParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext)
756-
: BindComplexParameterFromFormItem(parameter, formAttribute.Name ?? parameter.Name, factoryContext);
761+
: BindComplexParameterFromFormItem(parameter, string.IsNullOrEmpty(formAttribute.Name) ? parameter.Name : formAttribute.Name, factoryContext);
757762
}
758763
else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType)))
759764
{
@@ -1982,14 +1987,40 @@ private static Expression BindParameterFromFormItem(
19821987
"form");
19831988
}
19841989

1990+
private static void UpdateFormBindingArgumentExpressions(RequestDelegateFactoryContext factoryContext)
1991+
{
1992+
if (factoryContext.ArgumentExpressions == null || factoryContext.ArgumentExpressions.Length == 0)
1993+
{
1994+
return;
1995+
}
1996+
1997+
for (var i = 0; i < factoryContext.ArgumentExpressions.Length; i++)
1998+
{
1999+
var parameter = factoryContext.Parameters[i];
2000+
var key = parameter.Name!;
2001+
if (factoryContext.TrackedParameters.TryGetValue(key, out var trackedParameter) && trackedParameter == RequestDelegateFactoryConstants.FormBindingAttribute)
2002+
{
2003+
factoryContext.ArgumentExpressions[i] = BindComplexParameterFromFormItem(parameter, key, factoryContext);
2004+
}
2005+
}
2006+
}
2007+
19852008
private static Expression BindComplexParameterFromFormItem(
19862009
ParameterInfo parameter,
19872010
string key,
19882011
RequestDelegateFactoryContext factoryContext)
19892012
{
19902013
factoryContext.FirstFormRequestBodyParameter ??= parameter;
1991-
factoryContext.TrackedParameters.Add(key, RequestDelegateFactoryConstants.FormAttribute);
2014+
factoryContext.TrackedParameters.TryAdd(key, RequestDelegateFactoryConstants.FormBindingAttribute);
19922015
factoryContext.ReadForm = true;
2016+
var formDataMapperOptions = FormDataMapperOptions;
2017+
var formMappingOptionsMetadatas = factoryContext.EndpointBuilder.Metadata.OfType<FormMappingOptionsMetadata>();
2018+
foreach (var formMappingOptionsMetadata in formMappingOptionsMetadatas)
2019+
{
2020+
formDataMapperOptions.MaxRecursionDepth = formMappingOptionsMetadata.MaxRecursionDepth ?? formDataMapperOptions.MaxRecursionDepth;
2021+
formDataMapperOptions.MaxCollectionSize = formMappingOptionsMetadata.MaxCollectionSize ?? formDataMapperOptions.MaxCollectionSize;
2022+
formDataMapperOptions.MaxKeyBufferSize = formMappingOptionsMetadata.MaxKeySize ?? formDataMapperOptions.MaxKeyBufferSize;
2023+
}
19932024

19942025
// var name_local;
19952026
// var name_reader;
@@ -2001,19 +2032,19 @@ private static Expression BindComplexParameterFromFormItem(
20012032
var formBuffer = Expression.Variable(typeof(char[]), "form_buffer");
20022033

20032034
// ProcessForm(context.Request.Form, form_dict, form_buffer);
2004-
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, formDict, formBuffer);
2035+
var processFormExpr = Expression.Call(ProcessFormMethod, FormExpr, Expression.Constant(formDataMapperOptions.MaxKeyBufferSize), formDict, formBuffer);
20052036
// name_reader = new FormDataReader(form_dict, CultureInfo.InvariantCulture, form_buffer.AsMemory(0, FormDataMapperOptions.MaxKeyBufferSize));
20062037
var initializeReaderExpr = Expression.Assign(
20072038
formReader,
20082039
Expression.New(FormDataReaderConstructor,
20092040
formDict,
20102041
Expression.Constant(CultureInfo.InvariantCulture),
2011-
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(FormDataMapperOptions.MaxKeyBufferSize))));
2042+
Expression.Call(AsMemoryMethod, formBuffer, Expression.Constant(0), Expression.Constant(formDataMapperOptions.MaxKeyBufferSize))));
20122043
// FormDataMapper.Map<string>(name_reader, FormDataMapperOptions);
20132044
var invokeMapMethodExpr = Expression.Call(
20142045
FormDataMapperMapMethod.MakeGenericMethod(parameter.ParameterType),
20152046
formReader,
2016-
Expression.Constant(FormDataMapperOptions));
2047+
Expression.Constant(formDataMapperOptions));
20172048
// if (form_buffer != null)
20182049
// {
20192050
// ArrayPool<char>.Shared.Return(form_buffer, false);
@@ -2039,15 +2070,15 @@ private static Expression BindComplexParameterFromFormItem(
20392070
);
20402071
}
20412072

2042-
private static void ProcessForm(IFormCollection form, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
2073+
private static void ProcessForm(IFormCollection form, int maxKeyBufferSize, ref IReadOnlyDictionary<FormKey, StringValues> formDictionary, ref char[] buffer)
20432074
{
20442075
var dictionary = new Dictionary<FormKey, StringValues>();
20452076
foreach (var (key, value) in form)
20462077
{
20472078
dictionary.Add(new FormKey(key.AsMemory()), value);
20482079
}
20492080
formDictionary = dictionary.AsReadOnly();
2050-
buffer = ArrayPool<char>.Shared.Rent(FormDataMapperOptions.MaxKeyBufferSize);
2081+
buffer = ArrayPool<char>.Shared.Rent(maxKeyBufferSize);
20512082
}
20522083

20532084
private static Expression BindParameterFromFormFiles(
@@ -2447,6 +2478,7 @@ private static class RequestDelegateFactoryConstants
24472478
public const string ServiceAttribute = "Service (Attribute)";
24482479
public const string FormFileAttribute = "Form File (Attribute)";
24492480
public const string FormAttribute = "Form (Attribute)";
2481+
public const string FormBindingAttribute = "Form Binding (Attribute)";
24502482
public const string RouteParameter = "Route (Inferred)";
24512483
public const string QueryStringParameter = "Query String (Inferred)";
24522484
public const string ServiceParameter = "Services (Inferred)";

0 commit comments

Comments
 (0)