Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWilliam Godbe <wigodbe@microsoft.com>2022-11-11 00:21:18 +0300
committerGitHub <noreply@github.com>2022-11-11 00:21:18 +0300
commit53f44b989b2f2ce2f5e29472b4b4263b680de761 (patch)
tree2d67f24a7c28ffa45fa85686be725a8f407dc9f4
parentd5bd3ef932e1b4c0edc46244eb76cf1d512d09fc (diff)
parentc44c1145a3a06d9999a7d6fa0e278351ab8bb161 (diff)
Merge pull request #44951 from wtgodbe/wtgodbe/2139release/2.1
Update branding to 2.1.39, merge internal commits
-rw-r--r--build/dependencies.props8
-rw-r--r--eng/Baseline.Designer.props10
-rw-r--r--eng/Baseline.xml8
-rw-r--r--eng/Dependencies.props1
-rw-r--r--eng/PatchConfig.props8
-rw-r--r--src/Mvc/Directory.Build.props6
-rw-r--r--src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj1
-rw-r--r--src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs50
-rw-r--r--src/Mvc/Mvc.Abstractions/src/Properties/Resources.Designer.cs14
-rw-r--r--src/Mvc/Mvc.Abstractions/src/Resources.resx3
-rw-r--r--src/Mvc/Mvc.Abstractions/test/Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj1
-rw-r--r--src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs157
-rw-r--r--src/Mvc/Mvc.Core/src/Internal/DefaultModelBindingContext.cs39
-rw-r--r--src/Mvc/Mvc.Core/src/Internal/DefaultObjectValidator.cs11
-rw-r--r--src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj1
-rw-r--r--src/Mvc/Mvc.Core/src/ModelBinding/Binders/CollectionModelBinder.cs33
-rw-r--r--src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs65
-rw-r--r--src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs74
-rw-r--r--src/Mvc/Mvc.Core/src/Resources.resx17
-rw-r--r--src/Mvc/Mvc.Core/test/Internal/DefaultObjectValidatorTests.cs90
-rw-r--r--src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj2
-rw-r--r--src/Mvc/Mvc.Core/test/ModelBinding/Binders/CollectionModelBinderTest.cs54
-rw-r--r--src/Mvc/Mvc.Core/test/ModelBinding/DefaultModelBindingContextTest.cs47
-rw-r--r--src/Mvc/shared/ModelBindingSwitches.cs115
-rw-r--r--src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs20
-rw-r--r--src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj2
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs35
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs74
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs101
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs35
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj2
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs5
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/Models/SuccessfulModel.cs13
-rw-r--r--src/Mvc/test/Mvc.IntegrationTests/SuccessfulModelBinder.cs22
-rw-r--r--src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs6
-rw-r--r--src/Mvc/test/WebSites/FormatterWebSite/Models/InfinitelyRecursiveModel.cs29
-rw-r--r--src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs18
-rw-r--r--src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.37.txt1
-rw-r--r--src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.38.txt1
-rw-r--r--src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.37.txt1
-rw-r--r--src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.38.txt1
-rw-r--r--src/Security/Interop/src/ChunkingCookieManager.cs280
-rw-r--r--src/Security/Interop/src/Microsoft.Owin.Security.Interop.csproj7
-rw-r--r--src/Security/Interop/test/CookieChunkingTests.cs239
-rw-r--r--src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs61
-rw-r--r--version.props4
46 files changed, 1453 insertions, 319 deletions
diff --git a/build/dependencies.props b/build/dependencies.props
index a6af270e79..ba8eed1d62 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -1,4 +1,4 @@
-<Project>
+<Project>
<!-- These package versions may be overridden or updated by automation. -->
<PropertyGroup Label="Package Versions: Auto" Condition=" '$(DotNetPackageVersionPropsPath)' == '' ">
<!-- MicrosoftNETCoreApp21PackageVersion is assigned at the bottom so it can automatically pick up MicrosoftNETCoreAppPackageVersion in an orchestrated build. -->
@@ -137,9 +137,9 @@
<MicrosoftNETCoreApp20PackageVersion>2.0.9</MicrosoftNETCoreApp20PackageVersion>
<MicrosoftNETCoreWindowsApiSetsPackageVersion>1.0.1</MicrosoftNETCoreWindowsApiSetsPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.9.2</MicrosoftNETTestSdkPackageVersion>
- <MicrosoftOwinSecurityCookiesPackageVersion>3.0.1</MicrosoftOwinSecurityCookiesPackageVersion>
- <MicrosoftOwinSecurityPackageVersion>3.0.1</MicrosoftOwinSecurityPackageVersion>
- <MicrosoftOwinTestingPackageVersion>3.0.1</MicrosoftOwinTestingPackageVersion>
+ <MicrosoftOwinSecurityCookiesPackageVersion>4.2.2</MicrosoftOwinSecurityCookiesPackageVersion>
+ <MicrosoftOwinSecurityPackageVersion>4.2.2</MicrosoftOwinSecurityPackageVersion>
+ <MicrosoftOwinTestingPackageVersion>4.2.2</MicrosoftOwinTestingPackageVersion>
<MicrosoftVisualStudioComponentModelHostPackageVersion>15.0.26606</MicrosoftVisualStudioComponentModelHostPackageVersion>
<MicrosoftVisualStudioEditorPackageVersion>15.6.27740</MicrosoftVisualStudioEditorPackageVersion>
<MicrosoftVisualStudioLanguageIntellisensePackageVersion>15.6.27740</MicrosoftVisualStudioLanguageIntellisensePackageVersion>
diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props
index 285c85dde2..96940240da 100644
--- a/eng/Baseline.Designer.props
+++ b/eng/Baseline.Designer.props
@@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
- <AspNetCoreBaselineVersion>2.1.36</AspNetCoreBaselineVersion>
+ <AspNetCoreBaselineVersion>2.1.38</AspNetCoreBaselineVersion>
</PropertyGroup>
<!-- Package: dotnet-dev-certs-->
<PropertyGroup Condition=" '$(PackageId)' == 'dotnet-dev-certs' ">
@@ -601,7 +601,7 @@
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.Mvc.Abstractions-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Mvc.Abstractions' ">
- <BaselinePackageVersion>2.1.3</BaselinePackageVersion>
+ <BaselinePackageVersion>2.1.38</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Mvc.Abstractions' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Routing.Abstractions" Version="[2.1.1, )" />
@@ -621,7 +621,7 @@
</ItemGroup>
<!-- Package: Microsoft.AspNetCore.Mvc.Core-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Mvc.Core' ">
- <BaselinePackageVersion>2.1.34</BaselinePackageVersion>
+ <BaselinePackageVersion>2.1.38</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.AspNetCore.Mvc.Core' AND '$(TargetFramework)' == 'netstandard2.0' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.Authentication.Core" Version="[2.1.1, )" />
@@ -1234,10 +1234,10 @@
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.Net.Sdk.Razor' AND '$(TargetFramework)' == 'netstandard2.0' " />
<!-- Package: Microsoft.Owin.Security.Interop-->
<PropertyGroup Condition=" '$(PackageId)' == 'Microsoft.Owin.Security.Interop' ">
- <BaselinePackageVersion>2.1.2</BaselinePackageVersion>
+ <BaselinePackageVersion>2.1.38</BaselinePackageVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(PackageId)' == 'Microsoft.Owin.Security.Interop' AND '$(TargetFramework)' == 'net461' ">
<BaselinePackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="[2.1.1, )" />
- <BaselinePackageReference Include="Microsoft.Owin.Security" Version="[3.0.1, )" />
+ <BaselinePackageReference Include="Microsoft.Owin.Security" Version="[4.2.2, )" />
</ItemGroup>
</Project> \ No newline at end of file
diff --git a/eng/Baseline.xml b/eng/Baseline.xml
index bb9372abea..a4c480bcb5 100644
--- a/eng/Baseline.xml
+++ b/eng/Baseline.xml
@@ -4,7 +4,7 @@ This file contains a list of all the packages and their versions which were rele
build of ASP.NET Core 2.1.x. Update this list when preparing for a new patch.
-->
-<Baseline Version="2.1.36">
+<Baseline Version="2.1.38">
<Package Id="dotnet-dev-certs" Version="2.1.1" />
<Package Id="dotnet-sql-cache" Version="2.1.1" />
<Package Id="dotnet-user-secrets" Version="2.1.1" />
@@ -67,10 +67,10 @@ build of ASP.NET Core 2.1.x. Update this list when preparing for a new patch.
<Package Id="Microsoft.AspNetCore.Localization.Routing" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.Localization" Version="2.1.1" />
<Package Id="Microsoft.AspNetCore.MiddlewareAnalysis" Version="2.1.1" />
- <Package Id="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.3" />
+ <Package Id="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.1.38" />
<Package Id="Microsoft.AspNetCore.Mvc.Analyzers" Version="2.1.3" />
<Package Id="Microsoft.AspNetCore.Mvc.ApiExplorer" Version="2.1.3" />
- <Package Id="Microsoft.AspNetCore.Mvc.Core" Version="2.1.34" />
+ <Package Id="Microsoft.AspNetCore.Mvc.Core" Version="2.1.38" />
<Package Id="Microsoft.AspNetCore.Mvc.Cors" Version="2.1.3" />
<Package Id="Microsoft.AspNetCore.Mvc.DataAnnotations" Version="2.1.3" />
<Package Id="Microsoft.AspNetCore.Mvc.Formatters.Json" Version="2.1.18" />
@@ -126,5 +126,5 @@ build of ASP.NET Core 2.1.x. Update this list when preparing for a new patch.
<Package Id="Microsoft.Extensions.Identity.Stores" Version="2.1.6" />
<Package Id="Microsoft.Net.Http.Headers" Version="2.1.14" />
<Package Id="Microsoft.Net.Sdk.Razor" Version="2.1.2" />
- <Package Id="Microsoft.Owin.Security.Interop" Version="2.1.2" />
+ <Package Id="Microsoft.Owin.Security.Interop" Version="2.1.38" />
</Baseline>
diff --git a/eng/Dependencies.props b/eng/Dependencies.props
index ed562c8690..6b7652098a 100644
--- a/eng/Dependencies.props
+++ b/eng/Dependencies.props
@@ -73,6 +73,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.Extensions.ValueStopWatch.Sources" Version="$(MicrosoftExtensionsValueStopwatchSourcesPackageVersion)" />
<LatestPackageReference Include="Microsoft.Extensions.WebEncoders" Version="$(MicrosoftExtensionsWebEncodersPackageVersion)" />
<LatestPackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="$(MicrosoftIdentityModelClientsActiveDirectoryPackageVersion)" />
+ <LatestPackageReference Include="Microsoft.Owin.Security" Version="$(MicrosoftOwinSecurityPackageVersion)" />
<LatestPackageReference Include="Microsoft.Owin.Security.Cookies" Version="$(MicrosoftOwinSecurityCookiesPackageVersion)" />
<LatestPackageReference Include="Microsoft.Owin.Testing" Version="$(MicrosoftOwinTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.NETCore.Windows.ApiSets" Version="$(MicrosoftNETCoreWindowsApiSetsPackageVersion)" />
diff --git a/eng/PatchConfig.props b/eng/PatchConfig.props
index 651c45023c..446d232bff 100644
--- a/eng/PatchConfig.props
+++ b/eng/PatchConfig.props
@@ -119,10 +119,18 @@ Later on, this will be checked using this condition:
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.37' ">
<PackagesInPatch>
+ Microsoft.AspNetCore.Mvc.Abstractions;
</PackagesInPatch>
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.38' ">
<PackagesInPatch>
+ Microsoft.AspNetCore.Mvc.Abstractions;
+ Microsoft.AspNetCore.Mvc.Core;
+ Microsoft.Owin.Security.Interop;
+ </PackagesInPatch>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.39' ">
+ <PackagesInPatch>
</PackagesInPatch>
</PropertyGroup>
</Project>
diff --git a/src/Mvc/Directory.Build.props b/src/Mvc/Directory.Build.props
index c1c5d2d1e0..138fe482f6 100644
--- a/src/Mvc/Directory.Build.props
+++ b/src/Mvc/Directory.Build.props
@@ -6,4 +6,10 @@
<WarningsNotAsErrors>xUnit1026:$(WarningsNotAsErrors)</WarningsNotAsErrors>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)MvcTests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
+
+ <!-- Source code settings -->
+ <PropertyGroup>
+ <MvcSharedSourceRoot>$(MSBuildThisFileDirectory)Shared\</MvcSharedSourceRoot>
+ </PropertyGroup>
+
</Project>
diff --git a/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj b/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
index 9fc6e08ada..a50167aabd 100644
--- a/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
+++ b/src/Mvc/Mvc.Abstractions/src/Microsoft.AspNetCore.Mvc.Abstractions.csproj
@@ -14,6 +14,7 @@ Microsoft.AspNetCore.Mvc.IActionResult</Description>
<Compile Include="$(SharedSourceRoot)ClosedGenericMatcher\*.cs" />
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
+ <Compile Include="$(MvcSharedSourceRoot)ModelBindingSwitches.cs" LinkBase="Shared" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs b/src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs
index 970e5b6975..b61a8ca1ec 100644
--- a/src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs
+++ b/src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelStateDictionary.cs
@@ -24,6 +24,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
public static readonly int DefaultMaxAllowedErrors = 200;
+ // internal for testing
+ internal const int DefaultMaxRecursionDepth = 32;
+
private const char DelimiterDot = '.';
private const char DelimiterOpen = '[';
@@ -42,8 +45,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
/// </summary>
public ModelStateDictionary(int maxAllowedErrors)
+ : this(maxAllowedErrors,
+ maxValidationDepth: ModelBindingSwitches.MaxModelStateValidationDepth,
+ maxStateDepth: ModelBindingSwitches.MaxStateDepth)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ModelStateDictionary"/> class.
+ /// </summary>
+ private ModelStateDictionary(int maxAllowedErrors, int maxValidationDepth, int maxStateDepth)
{
MaxAllowedErrors = maxAllowedErrors;
+ MaxValidationDepth = maxValidationDepth;
+ MaxStateDepth = maxStateDepth;
var emptySegment = new StringSegment(buffer: string.Empty);
_root = new ModelStateNode(subKey: emptySegment)
{
@@ -57,7 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// </summary>
/// <param name="dictionary">The <see cref="ModelStateDictionary"/> to copy values from.</param>
public ModelStateDictionary(ModelStateDictionary dictionary)
- : this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors)
+ : this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors,
+ dictionary?.MaxValidationDepth ?? ModelBindingSwitches.MaxModelStateValidationDepth,
+ dictionary?.MaxStateDepth ?? ModelBindingSwitches.MaxStateDepth)
{
if (dictionary == null)
{
@@ -152,7 +169,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
/// <inheritdoc />
- public ModelValidationState ValidationState => GetValidity(_root) ?? ModelValidationState.Valid;
+ public ModelValidationState ValidationState => GetValidity(_root, currentDepth: 0) ?? ModelValidationState.Valid;
/// <inheritdoc />
public ModelStateEntry this[string key]
@@ -172,6 +189,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Flag that indicates if TooManyModelErrorException has already been added to this dictionary.
private bool HasRecordedMaxModelError { get; set; }
+ internal int? MaxValidationDepth { get; set; }
+
+ internal int? MaxStateDepth { get; set; }
+
/// <summary>
/// Adds the specified <paramref name="exception"/> to the <see cref="ModelStateEntry.Errors"/> instance
/// that is associated with the specified <paramref name="key"/>. If the maximum number of allowed
@@ -208,7 +229,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return false;
}
- ErrorCount++;
AddModelErrorCore(key, exception);
return true;
}
@@ -316,7 +336,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return TryAddModelError(key, exception.Message);
}
- ErrorCount++;
AddModelErrorCore(key, exception);
return true;
}
@@ -374,13 +393,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return false;
}
- ErrorCount++;
var modelState = GetOrAddNode(key);
Count += !modelState.IsContainerNode ? 0 : 1;
modelState.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(errorMessage);
+ ErrorCount++;
return true;
}
@@ -400,7 +419,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
var item = GetNode(key);
- return GetValidity(item) ?? ModelValidationState.Unvalidated;
+ return GetValidity(item, currentDepth: 0) ?? ModelValidationState.Unvalidated;
}
/// <summary>
@@ -602,11 +621,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var current = _root;
if (key.Length > 0)
{
+ var currentDepth = 0;
var match = default(MatchResult);
do
{
+ if (MaxStateDepth != null && currentDepth >= MaxStateDepth)
+ {
+ throw new InvalidOperationException(Resources.FormatModelStateDictionary_MaxModelStateDepth(MaxStateDepth));
+ }
+
var subKey = FindNext(key, ref match);
current = current.GetOrAddNode(subKey);
+ currentDepth++;
} while (match.Type != Delimiter.None);
@@ -652,9 +678,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new StringSegment(key, keyStart, index - keyStart);
}
- private static ModelValidationState? GetValidity(ModelStateNode node)
+ private ModelValidationState? GetValidity(ModelStateNode node, int currentDepth)
{
- if (node == null)
+ if (node == null ||
+ (MaxValidationDepth != null && currentDepth >= MaxValidationDepth))
{
return null;
}
@@ -677,9 +704,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
if (node.ChildNodes != null)
{
+ currentDepth++;
+
for (var i = 0; i < node.ChildNodes.Count; i++)
{
- var entryState = GetValidity(node.ChildNodes[i]);
+ var entryState = GetValidity(node.ChildNodes[i], currentDepth);
if (entryState == ModelValidationState.Unvalidated)
{
@@ -703,7 +732,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var exception = new TooManyModelErrorsException(Resources.ModelStateDictionary_MaxModelStateErrors);
AddModelErrorCore(string.Empty, exception);
HasRecordedMaxModelError = true;
- ErrorCount++;
}
}
@@ -714,6 +742,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
modelState.ValidationState = ModelValidationState.Invalid;
modelState.MarkNonContainerNode();
modelState.Errors.Add(exception);
+
+ ErrorCount++;
}
/// <summary>
diff --git a/src/Mvc/Mvc.Abstractions/src/Properties/Resources.Designer.cs b/src/Mvc/Mvc.Abstractions/src/Properties/Resources.Designer.cs
index f19e063180..0462d05e31 100644
--- a/src/Mvc/Mvc.Abstractions/src/Properties/Resources.Designer.cs
+++ b/src/Mvc/Mvc.Abstractions/src/Properties/Resources.Designer.cs
@@ -81,6 +81,20 @@ namespace Microsoft.AspNetCore.Mvc.Abstractions
=> GetString("ModelStateDictionary_MaxModelStateErrors");
/// <summary>
+ /// The specified key exceeded the maximum ModelState depth: {0}
+ /// </summary>
+ internal static string ModelStateDictionary_MaxModelStateDepth
+ {
+ get => GetString("ModelStateDictionary_MaxModelStateDepth");
+ }
+
+ /// <summary>
+ /// The specified key exceeded the maximum ModelState depth: {0}
+ /// </summary>
+ internal static string FormatModelStateDictionary_MaxModelStateDepth(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ModelStateDictionary_MaxModelStateDepth"), p0);
+
+ /// <summary>
/// Body
/// </summary>
internal static string BindingSource_Body
diff --git a/src/Mvc/Mvc.Abstractions/src/Resources.resx b/src/Mvc/Mvc.Abstractions/src/Resources.resx
index 224ec4161d..5b33958f0c 100644
--- a/src/Mvc/Mvc.Abstractions/src/Resources.resx
+++ b/src/Mvc/Mvc.Abstractions/src/Resources.resx
@@ -174,4 +174,7 @@
<data name="BindingSource_FormFile" xml:space="preserve">
<value>FormFile</value>
</data>
+ <data name="ModelStateDictionary_MaxModelStateDepth" xml:space="preserve">
+ <value>The specified key exceeded the maximum ModelState depth: {0}</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/Mvc/Mvc.Abstractions/test/Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj b/src/Mvc/Mvc.Abstractions/test/Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj
index a1075f7f58..fbfafe02da 100644
--- a/src/Mvc/Mvc.Abstractions/test/Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj
+++ b/src/Mvc/Mvc.Abstractions/test/Microsoft.AspNetCore.Mvc.Abstractions.Test.csproj
@@ -6,6 +6,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Mvc.TestCommon" />
<Reference Include="System.Threading.Tasks.Extensions" />
</ItemGroup>
diff --git a/src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs b/src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs
index 75a01945d2..a3dd661dd4 100644
--- a/src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs
+++ b/src/Mvc/Mvc.Abstractions/test/ModelBinding/ModelStateDictionaryTest.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Linq;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
@@ -1519,6 +1520,162 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal("value1", property.RawValue);
}
+ [Fact]
+ public void GetFieldValidationState_ReturnsUnvalidated_IfTreeHeightIsGreaterThanLimit()
+ {
+ // Arrange
+ var stackLimit = 5;
+ var dictionary = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
+ dictionary.MaxValidationDepth = stackLimit;
+ dictionary.MaxStateDepth = null;
+ dictionary.MarkFieldValid(key);
+
+ // Act
+ var validationState = dictionary.GetFieldValidationState("foo");
+
+ // Assert
+ Assert.Equal(ModelValidationState.Unvalidated, validationState);
+ }
+
+ [Fact]
+ public void IsValidProperty_ReturnsTrue_IfTreeHeightIsGreaterThanLimit()
+ {
+ // Arrange
+ var stackLimit = 5;
+ var dictionary = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stackLimit + 1));
+ dictionary.MaxValidationDepth = stackLimit;
+ dictionary.MaxStateDepth = null;
+ dictionary.AddModelError(key, "some error");
+
+ // Act
+ var isValid = dictionary.IsValid;
+ var validationState = dictionary.ValidationState;
+
+ // Assert
+ Assert.True(isValid);
+ Assert.Equal(ModelValidationState.Valid, validationState);
+ }
+
+ [Fact]
+ public void TryAddModelException_Throws_IfKeyHasTooManySegments()
+ {
+ // Arrange
+ var exception = new TestException();
+
+ var stateDepth = 5;
+ var dictionary = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
+ dictionary.MaxStateDepth = stateDepth;
+
+ // Act
+ var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelException(key, exception));
+
+ // Assert
+ Assert.Equal(
+ $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
+ invalidException.Message);
+ }
+
+ [Fact]
+ public void TryAddModelError_Throws_IfKeyHasTooManySegments()
+ {
+ // Arrange
+ var stateDepth = 5;
+ var dictionary = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
+ dictionary.MaxStateDepth = stateDepth;
+
+ // Act
+ var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.TryAddModelError(key, "errorMessage"));
+
+ // Assert
+ Assert.Equal(
+ $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
+ invalidException.Message);
+ }
+
+ [Fact]
+ public void SetModelValue_Throws_IfKeyHasTooManySegments()
+ {
+ var stateDepth = 5;
+ var dictionary = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
+ dictionary.MaxStateDepth = stateDepth;
+
+ // Act
+ var invalidException = Assert.Throws<InvalidOperationException>(() => dictionary.SetModelValue(key, string.Empty, string.Empty));
+
+ // Assert
+ Assert.Equal(
+ $"The specified key exceeded the maximum ModelState depth: {dictionary.MaxStateDepth}",
+ invalidException.Message);
+ }
+
+ [Fact]
+ public void MarkFieldValid_Throws_IfKeyHasTooManySegments()
+ {
+ // Arrange
+ var stateDepth = 5;
+ var source = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
+ source.MaxStateDepth = stateDepth;
+
+ // Act
+ var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldValid(key));
+
+ // Assert
+ Assert.Equal(
+ $"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
+ exception.Message);
+ }
+
+ [Fact]
+ public void MarkFieldSkipped_Throws_IfKeyHasTooManySegments()
+ {
+ // Arrange
+ var stateDepth = 5;
+ var source = new ModelStateDictionary();
+ var key = string.Join(".", Enumerable.Repeat("foo", stateDepth + 1));
+ source.MaxStateDepth = stateDepth;
+
+ // Act
+ var exception = Assert.Throws<InvalidOperationException>(() => source.MarkFieldSkipped(key));
+
+ // Assert
+ Assert.Equal(
+ $"The specified key exceeded the maximum ModelState depth: {source.MaxStateDepth}",
+ exception.Message);
+ }
+
+ [Fact]
+ public void Constructor_SetsDefaultRecursionDepth()
+ {
+ // Arrange && Act
+ var dictionary = new ModelStateDictionary();
+
+ // Assert
+ Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxValidationDepth);
+ Assert.Equal(ModelStateDictionary.DefaultMaxRecursionDepth, dictionary.MaxStateDepth);
+ }
+
+ [Fact]
+ public void CopyConstructor_PreservesRecursionDepth()
+ {
+ // Arrange
+ var dictionary = new ModelStateDictionary();
+ dictionary.MaxValidationDepth = 5;
+ dictionary.MaxStateDepth = 4;
+
+ // Act
+ var newDictionary = new ModelStateDictionary(dictionary);
+
+ // Assert
+ Assert.Equal(dictionary.MaxValidationDepth, newDictionary.MaxValidationDepth);
+ Assert.Equal(dictionary.MaxStateDepth, newDictionary.MaxStateDepth);
+ }
+
private class OptionsAccessor : IOptions<MvcOptions>
{
public MvcOptions Value { get; } = new MvcOptions();
diff --git a/src/Mvc/Mvc.Core/src/Internal/DefaultModelBindingContext.cs b/src/Mvc/Mvc.Core/src/Internal/DefaultModelBindingContext.cs
index 0ff9292a02..5601a809c7 100644
--- a/src/Mvc/Mvc.Core/src/Internal/DefaultModelBindingContext.cs
+++ b/src/Mvc/Mvc.Core/src/Internal/DefaultModelBindingContext.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@@ -18,6 +19,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 +186,26 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
}
+ // Internal for testing
+ internal int MaxModelBindingRecursionDepth
+ {
+ get
+ {
+ if (!_maxModelBindingRecursionDepth.HasValue)
+ {
+ // Ignore incomplete initialization. This must be a test scenario because CreateBindingContext(...)
+ // has not been called.
+ _maxModelBindingRecursionDepth = ModelBindingSwitches.MaxRecursionDepth;
+ }
+
+ return _maxModelBindingRecursionDepth.Value;
+ }
+ set
+ {
+ _maxModelBindingRecursionDepth = value;
+ }
+ }
+
/// <summary>
/// Creates a new <see cref="DefaultModelBindingContext"/> for top-level model binding operation.
/// </summary>
@@ -246,6 +268,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ValueProvider = FilterValueProvider(valueProvider, bindingSource),
ValidationState = new ValidationStateDictionary(),
+
+ MaxModelBindingRecursionDepth = ModelBindingSwitches.MaxRecursionDepth,
};
}
@@ -298,6 +322,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(MaxModelBindingRecursionDepth),
+ MaxModelBindingRecursionDepth,
+ rootModelType,
+ ModelBindingSwitches.MaxRecursionDepth_ConfigKeyName));
+ }
+
Result = default;
return new NestedScope(this);
diff --git a/src/Mvc/Mvc.Core/src/Internal/DefaultObjectValidator.cs b/src/Mvc/Mvc.Core/src/Internal/DefaultObjectValidator.cs
index 0afcdb0f5e..f2530aeb8c 100644
--- a/src/Mvc/Mvc.Core/src/Internal/DefaultObjectValidator.cs
+++ b/src/Mvc/Mvc.Core/src/Internal/DefaultObjectValidator.cs
@@ -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.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -22,8 +23,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IList<IModelValidatorProvider> validatorProviders)
: base(modelMetadataProvider, validatorProviders)
{
+ MaxValidationDepth = ModelBindingSwitches.MaxValidationDepth;
}
+ // Internal for testing
+ internal int MaxValidationDepth { get; set; }
+
public override ValidationVisitor GetValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
@@ -31,12 +36,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
{
- return new ValidationVisitor(
+ var visitor = new ValidationVisitor(
actionContext,
validatorProvider,
validatorCache,
metadataProvider,
validationState);
+
+ visitor.MaxValidationDepth = MaxValidationDepth;
+
+ return visitor;
}
}
}
diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj
index 86bf66eef4..844ecb23ae 100644
--- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj
+++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj
@@ -23,6 +23,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute</Description>
<Compile Include="$(SharedSourceRoot)PropertyHelper\*.cs" />
<Compile Include="$(SharedSourceRoot)RangeHelper\**\*.cs" />
<Compile Include="$(SharedSourceRoot)SecurityHelper\**\*.cs" />
+ <Compile Include="$(MvcSharedSourceRoot)ModelBindingSwitches.cs" LinkBase="Shared" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CollectionModelBinder.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CollectionModelBinder.cs
index 16834ebadc..0f3134ce79 100644
--- a/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CollectionModelBinder.cs
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/Binders/CollectionModelBinder.cs
@@ -9,8 +9,10 @@ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -58,6 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
ElementBinder = elementBinder;
Logger = loggerFactory.CreateLogger(GetType());
+ MaxModelBindingCollectionSize = ModelBindingSwitches.MaxCollectionSize;
}
/// <summary>
@@ -70,6 +73,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
/// </summary>
protected ILogger Logger { get; }
+ // Internal for testing
+ internal int MaxModelBindingCollectionSize { get; set; }
+
/// <inheritdoc />
public virtual async Task BindModelAsync(ModelBindingContext bindingContext)
{
@@ -272,8 +278,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;
@@ -312,6 +322,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.FieldName) &&
+ bindingContext.ModelMetadata.MetadataKind != ModelMetadataKind.Type ?
+ bindingContext.ModelMetadata.Name :
+ bindingContext.FieldName) : // This name may unfortunately be empty.
+ bindingContext.ModelName;
+
+ throw new InvalidOperationException(Resources.FormatModelBinding_ExceededMaxModelBindingCollectionSize(
+ name,
+ nameof(MaxModelBindingCollectionSize),
+ MaxModelBindingCollectionSize,
+ bindingContext.ModelMetadata.ElementType,
+ ModelBindingSwitches.MaxCollectionSize_ConfigKeyName));
+ }
+
return new CollectionResult
{
Model = boundCollection,
diff --git a/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs b/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs
index 499959df27..adc265843f 100644
--- a/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs
+++ b/src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs
@@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
@@ -15,6 +17,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary>
public class ValidationVisitor
{
+ private int? _maxValidationDepth;
+
/// <summary>
/// Creates a new <see cref="ValidationVisitor"/>.
/// </summary>
@@ -71,9 +75,43 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
protected IValidationStrategy Strategy { get; set; }
/// <summary>
+ /// Gets or sets the maximum depth to constrain the validation visitor when validating.
+ /// <para>
+ /// <see cref="ValidationVisitor"/> traverses the object graph of the model being validated. For models
+ /// that are very deep or are infinitely recursive, validation may result in stack overflow.
+ /// </para>
+ /// <para>
+ /// When not <see langword="null"/>, <see cref="Visit(ModelMetadata, string, object)"/> will throw if
+ /// current traversal depth exceeds the specified value.
+ /// </para>
+ /// </summary>
+ internal int MaxValidationDepth
+ {
+ get
+ {
+ if (!_maxValidationDepth.HasValue)
+ {
+ _maxValidationDepth = ModelBindingSwitches.MaxValidationDepth;
+ }
+
+ return _maxValidationDepth.Value;
+ }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _maxValidationDepth = value;
+ }
+ }
+
+ /// <summary>
/// Indicates whether validation of a complex type should be performed if validation fails for any of its children. The default behavior is false.
/// </summary>
public bool ValidateComplexTypesIfChildValidationFails { get; set; }
+
/// <summary>
/// Validates a object.
/// </summary>
@@ -182,6 +220,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
return true;
}
+ if (CurrentPath.Count > MaxValidationDepth)
+ {
+ // Non cyclic but too deep an object graph.
+
+ // Pop the current model to make ValidationStack.Dispose happy
+ CurrentPath.Pop(model);
+
+ string message;
+ switch (metadata.MetadataKind)
+ {
+ case ModelMetadataKind.Property:
+ message = Resources.FormatValidationVisitor_ExceededMaxPropertyDepth(nameof(ValidationVisitor), MaxValidationDepth, metadata.Name, metadata.ContainerType);
+ break;
+
+ default:
+ // Since the minimum depth is never 0, MetadataKind can never be Parameter. Consequently we only special case MetadataKind.Property.
+ message = Resources.FormatValidationVisitor_ExceededMaxDepth(nameof(ValidationVisitor), MaxValidationDepth, metadata.ModelType);
+ break;
+ }
+
+ message += " " + Resources.FormatValidationVisitor_ExceededMaxDepthFix(ModelBindingSwitches.MaxValidationDepth_ConfigKeyName);
+ throw new InvalidOperationException(message)
+ {
+ HelpLink = "https://aka.ms/AA21ue1",
+ };
+ }
+
var entry = GetValidationEntry(model);
key = entry?.Key ?? key ?? string.Empty;
metadata = entry?.Metadata ?? metadata;
diff --git a/src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs b/src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs
index d60a46fe66..8b1f9b95f2 100644
--- a/src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs
+++ b/src/Mvc/Mvc.Core/src/Properties/Resources.Designer.cs
@@ -1453,7 +1453,7 @@ namespace Microsoft.AspNetCore.Mvc.Core
=> string.Format(CultureInfo.CurrentCulture, GetString("ComplexTypeModelBinder_NoParameterlessConstructor_ForParameter"), p0, p1);
/// <summary>
- /// Action '{0}' has more than one parameter that were specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use '{1}' to specify query string bound, '{2}' to specify route bound, and '{3}' for parameters to be bound from body:
+ /// Action '{0}' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use '{1}' to specify bound from query, '{2}' to specify bound from route, and '{3}' for parameters to be bound from body:
/// </summary>
internal static string ApiController_MultipleBodyParametersFound
{
@@ -1461,11 +1461,81 @@ namespace Microsoft.AspNetCore.Mvc.Core
}
/// <summary>
- /// Action '{0}' has more than one parameter that were specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use '{1}' to specify query string bound, '{2}' to specify route bound, and '{3}' for parameters to be bound from body:
+ /// Action '{0}' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use '{1}' to specify bound from query, '{2}' to specify bound from route, and '{3}' for parameters to be bound from body:
/// </summary>
internal static string FormatApiController_MultipleBodyParametersFound(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("ApiController_MultipleBodyParametersFound"), p0, p1, p2, p3);
+ /// <summary>
+ /// Collection bound to '{0}' exceeded {1} ({2}). This limit is a safeguard against incorrect model binders and models. Address issues in '{3}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{4}' to change the default size.
+ /// </summary>
+ internal static string ModelBinding_ExceededMaxModelBindingCollectionSize
+ {
+ get => GetString("ModelBinding_ExceededMaxModelBindingCollectionSize");
+ }
+
+ /// <summary>
+ /// Collection bound to '{0}' exceeded {1} ({2}). This limit is a safeguard against incorrect model binders and models. Address issues in '{3}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{4}' to change the default size.
+ /// </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}). Reduce the potential nesting of '{2}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{3}' to change the default depth.
+ /// </summary>
+ internal static string ModelBinding_ExceededMaxModelBindingRecursionDepth
+ {
+ get => GetString("ModelBinding_ExceededMaxModelBindingRecursionDepth");
+ }
+
+ /// <summary>
+ /// Model binding system exceeded {0} ({1}). Reduce the potential nesting of '{2}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{3}' to change the default depth.
+ /// </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);
+
+ /// <summary>
+ /// {0} exceeded the maximum configured validation depth '{1}' when validating type '{2}'.
+ /// </summary>
+ internal static string ValidationVisitor_ExceededMaxDepth
+ {
+ get => GetString("ValidationVisitor_ExceededMaxDepth");
+ }
+
+ /// <summary>
+ /// {0} exceeded the maximum configured validation depth '{1}' when validating type '{2}'.
+ /// </summary>
+ internal static string FormatValidationVisitor_ExceededMaxDepth(object p0, object p1, object p2)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ValidationVisitor_ExceededMaxDepth"), p0, p1, p2);
+
+ /// <summary>
+ /// This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch '{0}' or suppressing validation on the model type.
+ /// </summary>
+ internal static string ValidationVisitor_ExceededMaxDepthFix
+ {
+ get => GetString("ValidationVisitor_ExceededMaxDepthFix");
+ }
+
+ /// <summary>
+ /// This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch '{0}' or suppressing validation on the model type.
+ /// </summary>
+ internal static string FormatValidationVisitor_ExceededMaxDepthFix(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ValidationVisitor_ExceededMaxDepthFix"), p0);
+
+ /// <summary>
+ /// {0} exceeded the maximum configured validation depth '{1}' when validating property '{2}' on type '{3}'.
+ /// </summary>
+ internal static string ValidationVisitor_ExceededMaxPropertyDepth
+ {
+ get => GetString("ValidationVisitor_ExceededMaxPropertyDepth");
+ }
+
+ /// <summary>
+ /// {0} exceeded the maximum configured validation depth '{1}' when validating property '{2}' on type '{3}'.
+ /// </summary>
+ internal static string FormatValidationVisitor_ExceededMaxPropertyDepth(object p0, object p1, object p2, object p3)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ValidationVisitor_ExceededMaxPropertyDepth"), p0, p1, p2, p3);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Mvc/Mvc.Core/src/Resources.resx b/src/Mvc/Mvc.Core/src/Resources.resx
index 909febc6b4..b4e0aa363a 100644
--- a/src/Mvc/Mvc.Core/src/Resources.resx
+++ b/src/Mvc/Mvc.Core/src/Resources.resx
@@ -442,4 +442,21 @@
<data name="ApiController_MultipleBodyParametersFound" xml:space="preserve">
<value>Action '{0}' has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use '{1}' to specify bound from query, '{2}' to specify bound from route, and '{3}' for parameters to be bound from body:</value>
</data>
+ <data name="ModelBinding_ExceededMaxModelBindingCollectionSize" xml:space="preserve">
+ <value>Collection bound to '{0}' exceeded {1} ({2}). This limit is a safeguard against incorrect model binders and models. Address issues in '{3}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{4}' to change the default size.</value>
+ <comment>{0} is model name, {1} is MaxModelBindingCollectionSize, {2} is option value, {3} is affected type, {4} is MaxModelBindingCollectionSize switch name.</comment>
+ </data>
+ <data name="ModelBinding_ExceededMaxModelBindingRecursionDepth" xml:space="preserve">
+ <value>Model binding system exceeded {0} ({1}). Reduce the potential nesting of '{2}'. For example, this type may have a property with a model binder that always succeeds. Otherwise, consider setting the AppContext switch '{3}' to change the default depth.</value>
+ <comment>{0} is MaxModelBindingRecursionDepth, {1} is option value, {2} is (loopy or deeply nested) top-level model type, {3} is MaxModelBindingRecursionDepth switch name.</comment>
+ </data>
+ <data name="ValidationVisitor_ExceededMaxDepth" xml:space="preserve">
+ <value>{0} exceeded the maximum configured validation depth '{1}' when validating type '{2}'.</value>
+ </data>
+ <data name="ValidationVisitor_ExceededMaxDepthFix" xml:space="preserve">
+ <value>This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch '{0}' or suppressing validation on the model type.</value>
+ </data>
+ <data name="ValidationVisitor_ExceededMaxPropertyDepth" xml:space="preserve">
+ <value>{0} exceeded the maximum configured validation depth '{1}' when validating property '{2}' on type '{3}'.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/Mvc/Mvc.Core/test/Internal/DefaultObjectValidatorTests.cs b/src/Mvc/Mvc.Core/test/Internal/DefaultObjectValidatorTests.cs
index 5371633029..ea79e61b8a 100644
--- a/src/Mvc/Mvc.Core/test/Internal/DefaultObjectValidatorTests.cs
+++ b/src/Mvc/Mvc.Core/test/Internal/DefaultObjectValidatorTests.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -1195,6 +1196,68 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Empty(entry.Value.Errors);
}
+ [Fact]
+ public void Validate_Throws_IfValidationDepthExceedsMaxDepth()
+ {
+ // Arrange
+ var maxDepth = 5;
+ var expected = $"ValidationVisitor exceeded the maximum configured validation depth '{maxDepth}' when validating property '{nameof(DepthObject.Depth)}' on type '{typeof(DepthObject)}'. " +
+ "This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxValidationDepth' or suppressing validation on the model type.";
+ var actionContext = new ActionContext();
+ var validator = CreateValidator();
+ validator.MaxValidationDepth = maxDepth;
+ var model = new DepthObject(maxDepth);
+ var validationState = new ValidationStateDictionary
+ {
+ { model, new ValidationStateEntry() }
+ };
+
+ // Act & Assert
+ var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
+ Assert.Equal(expected, ex.Message);
+ }
+
+ [Fact]
+ public void Validate_WorksIfObjectGraphIsSmallerThanMaxDepth()
+ {
+ // Arrange
+ var maxDepth = 5;
+ var actionContext = new ActionContext();
+ var validator = CreateValidator();
+ validator.MaxValidationDepth = maxDepth;
+ var model = new DepthObject(maxDepth - 1);
+ var validationState = new ValidationStateDictionary
+ {
+ { model, new ValidationStateEntry() }
+ };
+
+ // Act & Assert
+ validator.Validate(actionContext, validationState, prefix: string.Empty, model);
+ Assert.True(actionContext.ModelState.IsValid);
+ }
+
+ [Fact]
+ public void Validate_Throws_WithMaxDepth_1()
+ {
+ // Arrange
+ var maxDepth = 1;
+ var expected = $"ValidationVisitor exceeded the maximum configured validation depth '{maxDepth}' when validating property '{nameof(DepthObject.Depth)}' on type '{typeof(DepthObject)}'. " +
+ "This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxValidationDepth' or suppressing validation on the model type.";
+ var actionContext = new ActionContext();
+ var validator = CreateValidator();
+ validator.MaxValidationDepth = maxDepth;
+ var model = new DepthObject(maxDepth + 1);
+ var validationState = new ValidationStateDictionary
+ {
+ { model, new ValidationStateEntry() }
+ };
+
+ // Act & Assert
+ var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
+ Assert.Equal(expected, ex.Message);
+ Assert.NotNull(ex.HelpLink);
+ }
+
private static DefaultObjectValidator CreateValidator(Type excludedType)
{
var excludeFilters = new List<SuppressChildValidationMetadataProvider>();
@@ -1220,6 +1283,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal<string>(keys.OrderBy(k => k).ToArray(), modelState.Keys.OrderBy(k => k).ToArray());
}
+ private void Validate_Throws_ForTopLeveleMetadataData(DepthObject depthObject) { }
+
private class ThrowingProperty
{
public string WatchOut
@@ -1379,5 +1444,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
public string City { get; set; }
}
+
+ private class DepthObject
+ {
+ public DepthObject(int maxAllowedDepth, int depth = 0)
+ {
+ MaxAllowedDepth = maxAllowedDepth;
+ Depth = depth;
+ }
+
+ public int Depth { get; }
+ public int MaxAllowedDepth { get; }
+
+ public DepthObject Instance
+ {
+ get
+ {
+ if (Depth == MaxAllowedDepth - 1)
+ {
+ return this;
+ }
+
+ return new DepthObject(MaxAllowedDepth, Depth + 1);
+ }
+ }
+ }
}
}
diff --git a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
index 7728fed2bb..1d2a56300e 100644
--- a/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
+++ b/src/Mvc/Mvc.Core/test/Microsoft.AspNetCore.Mvc.Core.Test.csproj
@@ -11,6 +11,8 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.TestCommon" />
<Reference Include="Microsoft.AspNetCore.Mvc.TestDiagnosticListener" />
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/CollectionModelBinderTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/CollectionModelBinderTest.cs
index b34cd09da5..93a0deeb59 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/Binders/CollectionModelBinderTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/Binders/CollectionModelBinderTest.cs
@@ -65,6 +65,60 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.DoesNotContain(boundCollection, bindingContext.ValidationState.Keys);
}
+ // Ensure CollectionModelBinder allows MaxModelBindingCollectionSize items.
+
+ [Fact]
+ public async Task BindComplexCollectionFromIndexes_BindItems_WhenMaxModelBindingCollectionSizeSet()
+ {
+ // Arrange
+ var valueProvider = new SimpleValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "100" },
+ { "someName[2]", "200" },
+ };
+ var bindingContext = GetModelBindingContext(valueProvider);
+ var binder = new CollectionModelBinder<int>(CreateIntBinder(), NullLoggerFactory.Instance);
+ binder.MaxModelBindingCollectionSize = valueProvider.Count;
+
+ // Act
+ var boundCollection = await binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null);
+
+ // Assert
+ Assert.Equal(new[] { 42, 100, 200 }, boundCollection.Model.ToArray());
+
+ // This uses the default IValidationStrategy
+ Assert.DoesNotContain(boundCollection, bindingContext.ValidationState.Keys);
+ }
+
+ // Ensure CollectionModelBinder disallows one more than MaxModelBindingCollectionSize items.
+ [Fact]
+ public async Task BindComplexCollectionFromIndexes_Throws_WhenMoreItemsThanMaxModelBindingCollectionSizeSet()
+ {
+ // Arrange
+ var expectedMessage = $"Collection bound to 'someName' exceeded " +
+ $"MaxModelBindingCollectionSize (3). This limit is a " +
+ $"safeguard against incorrect model binders and models. Address issues in " +
+ $"'{typeof(int)}'. For example, this type may have a " +
+ $"property with a model binder that always succeeds. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize' to change the default size.";
+ var valueProvider = new SimpleValueProvider
+ {
+ { "someName[0]", "42" },
+ { "someName[1]", "100" },
+ { "someName[2]", "200" },
+ { "someName[3]", "300" },
+ };
+ var bindingContext = GetModelBindingContext(valueProvider);
+ var binder = new CollectionModelBinder<int>(CreateIntBinder(), NullLoggerFactory.Instance);
+ binder.MaxModelBindingCollectionSize = valueProvider.Count - 1;
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync<InvalidOperationException>(
+ () => binder.BindComplexCollectionFromIndexes(bindingContext, indexNames: null));
+ Assert.Equal(expectedMessage, exception.Message);
+ }
+
[Theory]
[InlineData(false)]
[InlineData(true)]
diff --git a/src/Mvc/Mvc.Core/test/ModelBinding/DefaultModelBindingContextTest.cs b/src/Mvc/Mvc.Core/test/ModelBinding/DefaultModelBindingContextTest.cs
index 642082eb67..e8fef86bc0 100644
--- a/src/Mvc/Mvc.Core/test/ModelBinding/DefaultModelBindingContextTest.cs
+++ b/src/Mvc/Mvc.Core/test/ModelBinding/DefaultModelBindingContextTest.cs
@@ -145,6 +145,53 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
vp => Assert.Same(original[2], vp));
}
+ // Ensure model binding system disallows one more than MaxModelBindingRecursionDepth binders on the stack.
+ [Fact]
+ public void EnterNestedScope_Throws_WhenDeeperThanMaxModelBindingRecursionDepth()
+ {
+ // Arrange
+ var expectedMessage = $"Model binding system exceeded " +
+ $"MaxModelBindingRecursionDepth (3). Reduce the " +
+ $"potential nesting of '{typeof(string)}'. For example, this type may have a property with a " +
+ $"model binder that always succeeds. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxRecursionDepth' to change the default depth.";
+
+ var metadataProvider = new TestModelMetadataProvider();
+ metadataProvider
+ .ForProperty(typeof(string), nameof(string.Length))
+ .BindingDetails(b => b.BindingSource = BindingSource.Query);
+
+ var original = CreateDefaultValueProvider();
+ var context = DefaultModelBindingContext.CreateBindingContext(
+ GetActionContext(),
+ original,
+ metadataProvider.GetMetadataForType(typeof(string)),
+ new BindingInfo(),
+ "model") as DefaultModelBindingContext;
+ context.MaxModelBindingRecursionDepth = 3;
+
+ var propertyMetadata = metadataProvider.GetMetadataForProperty(typeof(string), nameof(string.Length));
+
+ void RecursiveNestedScope(int depth = 0)
+ {
+ if (depth >= context.MaxModelBindingRecursionDepth)
+ {
+ return;
+ }
+
+ using (context.EnterNestedScope(propertyMetadata, "Length", "Length", model: null))
+ {
+ RecursiveNestedScope(++depth);
+ }
+ }
+
+ // Act & Assert
+ var exception = Assert.Throws<InvalidOperationException>(
+ () => RecursiveNestedScope());
+ Assert.Equal(expectedMessage, exception.Message);
+ }
+
+
[Fact]
public void ModelTypeAreFedFromModelMetadata()
{
diff --git a/src/Mvc/shared/ModelBindingSwitches.cs b/src/Mvc/shared/ModelBindingSwitches.cs
new file mode 100644
index 0000000000..a44d6db7f4
--- /dev/null
+++ b/src/Mvc/shared/ModelBindingSwitches.cs
@@ -0,0 +1,115 @@
+// 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;
+
+namespace Microsoft.AspNetCore.Mvc.ModelBinding
+{
+ internal static class ModelBindingSwitches
+ {
+ private const int DefaultMaxRecursionDepth = 32;
+ private const int DefaultMaxCollectionSize = 1024;
+
+ internal const string MaxCollectionSize_ConfigKeyName = "Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize";
+ internal const string MaxRecursionDepth_ConfigKeyName = "Microsoft.AspNetCore.Mvc.ModelBinding.MaxRecursionDepth";
+ internal const string MaxValidationDepth_ConfigKeyName = "Microsoft.AspNetCore.Mvc.ModelBinding.MaxValidationDepth";
+
+ internal const string MaxModelStateValidationDepth_ConfigKeyName = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary.MaxValidationDepth";
+ internal const string MaxStateDepth_ConfigKeyName = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary.MaxStateDepth";
+
+ private static int? _maxModelStateValidationDepth;
+ private static int? _maxValidationDepth;
+ private static int? _maxStateDepth;
+ private static int? _maxRecursionDepth;
+ private static int? _maxCollectionSize;
+
+ public static int MaxValidationDepth
+ {
+ get
+ {
+ if (!_maxValidationDepth.HasValue)
+ {
+ var validationDepth = AppContext.GetData(MaxValidationDepth_ConfigKeyName);
+
+ _maxValidationDepth = (validationDepth is int validationDepthInt && validationDepthInt > 0) ?
+ validationDepthInt :
+ DefaultMaxRecursionDepth;
+ }
+
+ return _maxValidationDepth.Value;
+ }
+ }
+
+ public static int MaxRecursionDepth
+ {
+ get
+ {
+ if (!_maxRecursionDepth.HasValue)
+ {
+ var recursionDepth = AppContext.GetData(MaxRecursionDepth_ConfigKeyName);
+
+ _maxRecursionDepth = (recursionDepth is int recursionDepthInt && recursionDepthInt > 0) ?
+ recursionDepthInt :
+ DefaultMaxRecursionDepth;
+ }
+
+ return _maxRecursionDepth.Value;
+ }
+ }
+
+ public static int MaxCollectionSize
+ {
+ get
+ {
+ if (!_maxCollectionSize.HasValue)
+ {
+ var collectionSize = AppContext.GetData(MaxCollectionSize_ConfigKeyName);
+
+ _maxCollectionSize = (collectionSize is int collectionSizeInt && collectionSizeInt > 0) ?
+ collectionSizeInt :
+ DefaultMaxCollectionSize;
+ }
+
+ return _maxCollectionSize.Value;
+ }
+ }
+
+ // Switches ModelStateDictionary-specific
+
+ public static int MaxStateDepth
+ {
+ get
+ {
+ if (!_maxStateDepth.HasValue)
+ {
+ var stateDepth = AppContext.GetData(MaxStateDepth_ConfigKeyName);
+
+ _maxStateDepth = (stateDepth is int stateDepthInt && stateDepthInt > 0) ?
+ stateDepthInt :
+ // Fallback to the general Recursion Depth switch
+ MaxRecursionDepth;
+ }
+
+ return _maxStateDepth.Value;
+ }
+ }
+
+ public static int MaxModelStateValidationDepth
+ {
+ get
+ {
+ if (!_maxModelStateValidationDepth.HasValue)
+ {
+ var validationDepth = AppContext.GetData(MaxModelStateValidationDepth_ConfigKeyName);
+
+ _maxModelStateValidationDepth = (validationDepth is int validationDepthInt && validationDepthInt > 0) ?
+ validationDepthInt :
+ // Fallback to the general Validation switch
+ MaxValidationDepth;
+ }
+
+ return _maxModelStateValidationDepth.Value;
+ }
+ }
+ }
+}
diff --git a/src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs b/src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs
index dbe25c6c92..62036aab3d 100644
--- a/src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs
+++ b/src/Mvc/test/Mvc.FunctionalTests/InputObjectValidationTests.cs
@@ -1,11 +1,13 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
+using FormatterWebSite;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
@@ -182,5 +184,23 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("xyz", await response.Content.ReadAsStringAsync());
}
+
+
+ // Test for https://github.com/aspnet/Mvc/issues/7357
+ [Fact]
+ public async Task ValidationThrowsError_WhenValidationExceedsMaxValidationDepth()
+ {
+ // Arrange
+ var expected = $"ValidationVisitor exceeded the maximum configured validation depth '32' when validating property 'Value' on type '{typeof(RecursiveIdentifier)}'. " +
+ "This may indicate a very deep or infinitely recursive object graph. Consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxValidationDepth' or suppressing validation on the model type.";
+ var requestMessage = new HttpRequestMessage(HttpMethod.Post, "Validation/ValidationThrowsError_WhenValidationExceedsMaxValidationDepth")
+ {
+ Content = new StringContent(@"{ ""Id"": ""S-1-5-21-1004336348-1177238915-682003330-512"" }", Encoding.UTF8, "application/json"),
+ };
+
+ // Act & Assert
+ var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => Client.SendAsync(requestMessage));
+ Assert.Equal(expected, ex.Message);
+ }
}
} \ No newline at end of file
diff --git a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
index 70c14e3243..2f45fc4ea8 100644
--- a/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
+++ b/src/Mvc/test/Mvc.FunctionalTests/Microsoft.AspNetCore.Mvc.FunctionalTests.csproj
@@ -30,6 +30,8 @@
</ItemGroup>
<ItemGroup>
+ <Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.Testing" />
<Reference Include="Microsoft.AspNetCore.Razor.Runtime" />
<Reference Include="Microsoft.AspNetCore.Razor.Language" />
diff --git a/src/Mvc/test/Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs
index 921fa1f03f..d619cbec43 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/ArrayModelBinderIntegrationTest.cs
@@ -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,39 @@ 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 " +
+ $"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. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize' to change the default size.";
+
+ 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);
+ }
}
} \ No newline at end of file
diff --git a/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs
index 7cb0c782d2..4032541524 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/CollectionModelBinderIntegrationTest.cs
@@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.Extensions.Primitives;
using Xunit;
@@ -968,6 +969,79 @@ 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 " +
+ $"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. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize' to change the default size.";
+ 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);
+ }
+
+ private class SuccessfulContainer
+ {
+ public IList<SuccessfulModel> Successes { get; set; }
+ }
+
+ [Fact]
+ public async Task CollectionModelBinder_ThrowsOn1025Items()
+ {
+ // Arrange
+ var expectedMessage = $"Collection bound to 'Successes' exceeded " +
+ $"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. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize' to change the default size.";
+
+ 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>
{
}
diff --git a/src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs
index e3f693f58a..b67569446c 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs
@@ -2701,6 +2701,107 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.Equal("10,20", entry.RawValue);
}
+ private class LoopyModel
+ {
+ 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 " +
+ $"MaxModelBindingRecursionDepth (32). Reduce the " +
+ $"potential nesting of '{typeof(LoopyModel)}'. For example, this type may have a property with a " +
+ $"model binder that always succeeds. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxRecursionDepth' to change the default depth.";
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(LoopyModel),
+ };
+
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = CreateLoopyQueryString(nameof(LoopyModel.SelfReference), nameof(LoopyModel.IsBound))
+ );
+ 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 QueryString CreateLoopyQueryString(string prefix, string property)
+ {
+ var queryStringBuilder = new StringBuilder("?IsBound=true");
+
+ for (var i = 1; i < 32; i++)
+ {
+ queryStringBuilder.Append("&");
+ queryStringBuilder.Insert(queryStringBuilder.Length, $"{prefix}.", i);
+ queryStringBuilder.Append($"{property}=true");
+ }
+
+ return new QueryString(queryStringBuilder.ToString());
+ }
+
+ private class LoopyModel1
+ {
+ public bool IsBound { get; set; }
+
+ public LoopyModel2 Inner { get; set; }
+ }
+
+ private class LoopyModel2
+ {
+ public bool IsBound { get; set; }
+
+ public LoopyModel3 Inner { get; set; }
+ }
+
+ private class LoopyModel3
+ {
+ public bool IsBound { get; set; }
+
+ public LoopyModel1 Inner { get; set; }
+ }
+
+ [Fact]
+ public async Task ModelBindingSystem_ThrowsOn33Binders_WithIndirectModelTypeLoop()
+ {
+ // Arrange
+ var expectedMessage = $"Model binding system exceeded " +
+ $"MaxModelBindingRecursionDepth (32). Reduce the " +
+ $"potential nesting of '{typeof(LoopyModel1)}'. For example, this type may have a property with a " +
+ $"model binder that always succeeds. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxRecursionDepth' to change the default depth.";
+ var parameter = new ParameterDescriptor()
+ {
+ Name = "parameter",
+ ParameterType = typeof(LoopyModel1),
+ };
+
+ var testContext = ModelBindingTestHelper.GetTestContext(
+ request => request.QueryString = CreateLoopyQueryString(nameof(LoopyModel1.Inner), nameof(LoopyModel.IsBound))
+ );
+ 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));
diff --git a/src/Mvc/test/Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs b/src/Mvc/test/Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
index c19e7cfac5..325d1f3a5c 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/DictionaryModelBinderIntegrationTest.cs
@@ -1128,6 +1128,41 @@ 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 " +
+ $"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. " +
+ $"Otherwise, consider setting the AppContext switch 'Microsoft.AspNetCore.Mvc.ModelBinding.MaxCollectionSize' to change the default size.";
+
+ 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>
{
}
diff --git a/src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj b/src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj
index 283d2e7ce2..6c374e750a 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj
+++ b/src/Mvc/test/Mvc.IntegrationTests/Microsoft.AspNetCore.Mvc.IntegrationTests.csproj
@@ -6,6 +6,8 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
+ <Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.TestCommon" />
<Reference Include="Microsoft.AspNetCore.Http" />
diff --git a/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs b/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs
index 6aa9da7cf0..d73a1c12be 100644
--- a/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs
+++ b/src/Mvc/test/Mvc.IntegrationTests/ModelBindingTestHelper.cs
@@ -64,6 +64,11 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
}
}
+ public static ParameterBinder GetParameterBinder(ModelBindingTestContext testContext)
+ {
+ return GetParameterBinder(testContext.HttpContext.RequestServices);
+ }
+
public static ParameterBinder GetParameterBinder(IServiceProvider serviceProvider)
{
var metadataProvider = serviceProvider.GetRequiredService<IModelMetadataProvider>();
diff --git a/src/Mvc/test/Mvc.IntegrationTests/Models/SuccessfulModel.cs b/src/Mvc/test/Mvc.IntegrationTests/Models/SuccessfulModel.cs
new file mode 100644
index 0000000000..89c12544aa
--- /dev/null
+++ b/src/Mvc/test/Mvc.IntegrationTests/Models/SuccessfulModel.cs
@@ -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
+{
+ [ModelBinder(typeof(SuccessfulModelBinder))]
+ public class SuccessfulModel
+ {
+ public bool IsBound { get; set; }
+
+ public string Name { get; set; }
+ }
+} \ No newline at end of file
diff --git a/src/Mvc/test/Mvc.IntegrationTests/SuccessfulModelBinder.cs b/src/Mvc/test/Mvc.IntegrationTests/SuccessfulModelBinder.cs
new file mode 100644
index 0000000000..0536276404
--- /dev/null
+++ b/src/Mvc/test/Mvc.IntegrationTests/SuccessfulModelBinder.cs
@@ -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(SuccessfulModel) ? new SuccessfulModel() { IsBound = true } : null;
+ bindingContext.Result = ModelBindingResult.Success(model);
+
+ return Task.CompletedTask;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs
index efc2441c22..f6e65b154d 100644
--- a/src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs
+++ b/src/Mvc/test/WebSites/FormatterWebSite/Controllers/ValidationController.cs
@@ -67,5 +67,11 @@ namespace FormatterWebSite
{
return Json(simpleTypePropertiesModel);
}
+
+ [HttpPost]
+ public IActionResult ValidationThrowsError_WhenValidationExceedsMaxValidationDepth([FromBody] InfinitelyRecursiveModel model)
+ {
+ return Ok();
+ }
}
} \ No newline at end of file
diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Models/InfinitelyRecursiveModel.cs b/src/Mvc/test/WebSites/FormatterWebSite/Models/InfinitelyRecursiveModel.cs
new file mode 100644
index 0000000000..f534b40714
--- /dev/null
+++ b/src/Mvc/test/WebSites/FormatterWebSite/Models/InfinitelyRecursiveModel.cs
@@ -0,0 +1,29 @@
+// 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 Newtonsoft.Json;
+
+namespace FormatterWebSite
+{
+ public class InfinitelyRecursiveModel
+ {
+ [JsonConverter(typeof(StringIdentifierConverter))]
+ public RecursiveIdentifier Id { get; set; }
+
+ private class StringIdentifierConverter : JsonConverter
+ {
+ public override bool CanConvert(Type objectType) => objectType == typeof(RecursiveIdentifier);
+
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ return new RecursiveIdentifier(reader.Value.ToString());
+ }
+
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs b/src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs
new file mode 100644
index 0000000000..847a01b428
--- /dev/null
+++ b/src/Mvc/test/WebSites/FormatterWebSite/Models/RecursiveIdentifier.cs
@@ -0,0 +1,18 @@
+// 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.
+
+namespace FormatterWebSite
+{
+ // A System.Security.Principal.SecurityIdentifier like type that works on xplat
+ public class RecursiveIdentifier
+ {
+ public RecursiveIdentifier(string identifier)
+ {
+ Value = identifier;
+ }
+
+ public string Value { get; }
+
+ public RecursiveIdentifier AccountIdentifier => new RecursiveIdentifier(Value);
+ }
+} \ No newline at end of file
diff --git a/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.37.txt b/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.37.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.37.txt
@@ -0,0 +1 @@
+
diff --git a/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.38.txt b/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.38.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/PackageArchive/Archive.CiServer.Patch.Compat/ArchiveBaseline.2.1.38.txt
@@ -0,0 +1 @@
+
diff --git a/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.37.txt b/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.37.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.37.txt
@@ -0,0 +1 @@
+
diff --git a/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.38.txt b/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.38.txt
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/PackageArchive/Archive.CiServer.Patch/ArchiveBaseline.2.1.38.txt
@@ -0,0 +1 @@
+
diff --git a/src/Security/Interop/src/ChunkingCookieManager.cs b/src/Security/Interop/src/ChunkingCookieManager.cs
deleted file mode 100644
index 1ae4f00cdb..0000000000
--- a/src/Security/Interop/src/ChunkingCookieManager.cs
+++ /dev/null
@@ -1,280 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Microsoft.Owin.Infrastructure;
-
-namespace Microsoft.Owin.Security.Interop
-{
- // This MUST be kept in sync with Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager
- /// <summary>
- /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
- /// from requests.
- /// </summary>
- public class ChunkingCookieManager : ICookieManager
- {
- private const string ChunkKeySuffix = "C";
- private const string ChunkCountPrefix = "chunks-";
-
- public ChunkingCookieManager()
- {
- // Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case.
- // See http://browsercookielimits.x64.me/.
- // Leave at least 20 in case CookiePolicy tries to add 'secure' and/or 'httponly'.
- ChunkSize = 4070;
- }
-
- /// <summary>
- /// The maximum size of cookie to send back to the client. If a cookie exceeds this size it will be broken down into multiple
- /// cookies. Set this value to null to disable this behavior. The default is 4090 characters, which is supported by all
- /// common browsers.
- ///
- /// Note that browsers may also have limits on the total size of all cookies per domain, and on the number of cookies per domain.
- /// </summary>
- public int? ChunkSize { get; set; }
-
- /// <summary>
- /// Throw if not all chunks of a cookie are available on a request for re-assembly.
- /// </summary>
- public bool ThrowForPartialCookies { get; set; }
-
- // Parse the "chunks-XX" to determine how many chunks there should be.
- private static int ParseChunksCount(string value)
- {
- if (value != null && value.StartsWith(ChunkCountPrefix, StringComparison.Ordinal))
- {
- var chunksCountString = value.Substring(ChunkCountPrefix.Length);
- int chunksCount;
- if (int.TryParse(chunksCountString, NumberStyles.None, CultureInfo.InvariantCulture, out chunksCount))
- {
- return chunksCount;
- }
- }
- return 0;
- }
-
- /// <summary>
- /// Get the reassembled cookie. Non chunked cookies are returned normally.
- /// Cookies with missing chunks just have their "chunks-XX" header returned.
- /// </summary>
- /// <param name="context"></param>
- /// <param name="key"></param>
- /// <returns>The reassembled cookie, if any, or null.</returns>
- public string GetRequestCookie(IOwinContext context, string key)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (key == null)
- {
- throw new ArgumentNullException(nameof(key));
- }
-
- var requestCookies = context.Request.Cookies;
- var value = requestCookies[key];
- var chunksCount = ParseChunksCount(value);
- if (chunksCount > 0)
- {
- var chunks = new string[chunksCount];
- for (var chunkId = 1; chunkId <= chunksCount; chunkId++)
- {
- var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)];
- if (string.IsNullOrEmpty(chunk))
- {
- if (ThrowForPartialCookies)
- {
- var totalSize = 0;
- for (int i = 0; i < chunkId - 1; i++)
- {
- totalSize += chunks[i].Length;
- }
- throw new FormatException(
- string.Format(CultureInfo.CurrentCulture,
- "The chunked cookie is incomplete. Only {0} of the expected {1} chunks were found, totaling {2} characters. A client size limit may have been exceeded.",
- chunkId - 1, chunksCount, totalSize));
- }
- // Missing chunk, abort by returning the original cookie value. It may have been a false positive?
- return value;
- }
-
- chunks[chunkId - 1] = chunk;
- }
-
- return string.Join(string.Empty, chunks);
- }
- return value;
- }
-
- /// <summary>
- /// Appends a new response cookie to the Set-Cookie header. If the cookie is larger than the given size limit
- /// then it will be broken down into multiple cookies as follows:
- /// Set-Cookie: CookieName=chunks-3; path=/
- /// Set-Cookie: CookieNameC1=Segment1; path=/
- /// Set-Cookie: CookieNameC2=Segment2; path=/
- /// Set-Cookie: CookieNameC3=Segment3; path=/
- /// </summary>
- /// <param name="context"></param>
- /// <param name="key"></param>
- /// <param name="value"></param>
- /// <param name="options"></param>
- public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (key == null)
- {
- throw new ArgumentNullException(nameof(key));
- }
-
- if (options == null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- var domainHasValue = !string.IsNullOrEmpty(options.Domain);
- var pathHasValue = !string.IsNullOrEmpty(options.Path);
- var expiresHasValue = options.Expires.HasValue;
-
- var templateLength = key.Length + "=".Length
- + (domainHasValue ? "; domain=".Length + options.Domain.Length : 0)
- + (pathHasValue ? "; path=".Length + options.Path.Length : 0)
- + (expiresHasValue ? "; expires=ddd, dd-MMM-yyyy HH:mm:ss GMT".Length : 0)
- + (options.Secure ? "; secure".Length : 0)
- + (options.HttpOnly ? "; HttpOnly".Length : 0);
-
- // Normal cookie
- var responseCookies = context.Response.Cookies;
- if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + value.Length)
- {
- responseCookies.Append(key, value, options);
- }
- else if (ChunkSize.Value < templateLength + 10)
- {
- // 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX".
- // No room for data, we can't chunk the options and name
- throw new InvalidOperationException("The cookie key and options are larger than ChunksSize, leaving no room for data.");
- }
- else
- {
- // Break the cookie down into multiple cookies.
- // Key = CookieName, value = "Segment1Segment2Segment2"
- // Set-Cookie: CookieName=chunks-3; path=/
- // Set-Cookie: CookieNameC1="Segment1"; path=/
- // Set-Cookie: CookieNameC2="Segment2"; path=/
- // Set-Cookie: CookieNameC3="Segment3"; path=/
- var dataSizePerCookie = ChunkSize.Value - templateLength - 3; // Budget 3 chars for the chunkid.
- var cookieChunkCount = (int)Math.Ceiling(value.Length * 1.0 / dataSizePerCookie);
-
- responseCookies.Append(key, ChunkCountPrefix + cookieChunkCount.ToString(CultureInfo.InvariantCulture), options);
-
- var offset = 0;
- for (var chunkId = 1; chunkId <= cookieChunkCount; chunkId++)
- {
- var remainingLength = value.Length - offset;
- var length = Math.Min(dataSizePerCookie, remainingLength);
- var segment = value.Substring(offset, length);
- offset += length;
-
- responseCookies.Append(key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture), segment, options);
- }
- }
- }
-
- /// <summary>
- /// Deletes the cookie with the given key by setting an expired state. If a matching chunked cookie exists on
- /// the request, delete each chunk.
- /// </summary>
- /// <param name="context"></param>
- /// <param name="key"></param>
- /// <param name="options"></param>
- public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
- {
- if (context == null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- if (key == null)
- {
- throw new ArgumentNullException(nameof(key));
- }
-
- if (options == null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- var keys = new List<string>();
- keys.Add(key + "=");
-
- var requestCookie = context.Request.Cookies[key];
- var chunks = ParseChunksCount(requestCookie);
- if (chunks > 0)
- {
- for (int i = 1; i <= chunks + 1; i++)
- {
- var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture);
- keys.Add(subkey + "=");
- }
- }
-
- var domainHasValue = !string.IsNullOrEmpty(options.Domain);
- var pathHasValue = !string.IsNullOrEmpty(options.Path);
-
- Func<string, bool> rejectPredicate;
- Func<string, bool> predicate = value => keys.Any(k => value.StartsWith(k, StringComparison.OrdinalIgnoreCase));
- if (domainHasValue)
- {
- rejectPredicate = value => predicate(value) && value.IndexOf("domain=" + options.Domain, StringComparison.OrdinalIgnoreCase) != -1;
- }
- else if (pathHasValue)
- {
- rejectPredicate = value => predicate(value) && value.IndexOf("path=" + options.Path, StringComparison.OrdinalIgnoreCase) != -1;
- }
- else
- {
- rejectPredicate = value => predicate(value);
- }
-
- var responseHeaders = context.Response.Headers;
- string[] existingValues;
- if (responseHeaders.TryGetValue(Constants.Headers.SetCookie, out existingValues) && existingValues != null)
- {
- responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray());
- }
-
- AppendResponseCookie(
- context,
- key,
- string.Empty,
- new CookieOptions()
- {
- Path = options.Path,
- Domain = options.Domain,
- Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
- });
-
- for (int i = 1; i <= chunks; i++)
- {
- AppendResponseCookie(
- context,
- key + "C" + i.ToString(CultureInfo.InvariantCulture),
- string.Empty,
- new CookieOptions()
- {
- Path = options.Path,
- Domain = options.Domain,
- Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
- });
- }
- }
- }
-}
diff --git a/src/Security/Interop/src/Microsoft.Owin.Security.Interop.csproj b/src/Security/Interop/src/Microsoft.Owin.Security.Interop.csproj
index 1321c62664..1982b94dad 100644
--- a/src/Security/Interop/src/Microsoft.Owin.Security.Interop.csproj
+++ b/src/Security/Interop/src/Microsoft.Owin.Security.Interop.csproj
@@ -1,14 +1,19 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>A compatibility layer for sharing authentication tickets between Microsoft.Owin.Security and Microsoft.AspNetCore.Authentication.</Description>
<TargetFramework>net461</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
+ <DefineConstants>$(DefineConstants);INTEROP</DefineConstants>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;katana;owin;security</PackageTags>
</PropertyGroup>
<ItemGroup>
+ <Compile Include="$(SharedSourceRoot)ChunkingCookieManager\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />
<Reference Include="Microsoft.Owin.Security" />
</ItemGroup>
diff --git a/src/Security/Interop/test/CookieChunkingTests.cs b/src/Security/Interop/test/CookieChunkingTests.cs
new file mode 100644
index 0000000000..23035a13b2
--- /dev/null
+++ b/src/Security/Interop/test/CookieChunkingTests.cs
@@ -0,0 +1,239 @@
+// 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 Xunit;
+
+namespace Microsoft.Owin.Security.Interop
+{
+ public class CookieChunkingTests
+ {
+ [Fact]
+ public void AppendLargeCookie_Appended()
+ {
+ var context = new OwinContext();
+
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions() { SameSite = SameSiteMode.Lax });
+ var values = context.Response.Headers["Set-Cookie"];
+ Assert.Equal("TestCookie=" + testString + "; path=/; SameSite=Lax", values);
+ }
+
+ [Fact]
+ public void AppendLargeCookie_WithOptions_Appended()
+ {
+ var context = new OwinContext();
+ var now = DateTime.UtcNow;
+ var options = new CookieOptions
+ {
+ Domain = "foo.com",
+ HttpOnly = true,
+ SameSite = SameSiteMode.Strict,
+ Path = "/bar",
+ Secure = true,
+ Expires = now.AddMinutes(5),
+ };
+ var testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, options);
+
+ var values = context.Response.Headers["Set-Cookie"];
+ Assert.Equal($"TestCookie={testString}; domain=foo.com; path=/bar; expires={now.AddMinutes(5).ToString("ddd, dd-MMM-yyyy HH:mm:ss ")}GMT; secure; HttpOnly; SameSite=Strict", values);
+ }
+
+ [Fact]
+ public void AppendLargeCookieWithLimit_Chunked()
+ {
+ var context = new OwinContext();
+
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ new ChunkingCookieManager() { ChunkSize = 44 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions() { SameSite = SameSiteMode.Lax });
+ Assert.True(context.Response.Headers.TryGetValue("Set-Cookie", out var values));
+ Assert.Equal(9, values.Length);
+ Assert.Equal<string[]>(new[]
+ {
+ "TestCookie=chunks-8; path=/; SameSite=Lax",
+ "TestCookieC1=abcdefgh; path=/; SameSite=Lax",
+ "TestCookieC2=ijklmnop; path=/; SameSite=Lax",
+ "TestCookieC3=qrstuvwx; path=/; SameSite=Lax",
+ "TestCookieC4=yz012345; path=/; SameSite=Lax",
+ "TestCookieC5=6789ABCD; path=/; SameSite=Lax",
+ "TestCookieC6=EFGHIJKL; path=/; SameSite=Lax",
+ "TestCookieC7=MNOPQRST; path=/; SameSite=Lax",
+ "TestCookieC8=UVWXYZ; path=/; SameSite=Lax",
+ }, values);
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookie_Reassembled()
+ {
+ var context = new OwinContext();
+ context.Request.Headers.Add("Cookie", new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ });
+
+ string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
+ string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ Assert.Equal(testString, result);
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookieWithMissingChunk_ThrowingEnabled_Throws()
+ {
+ var context = new OwinContext();
+ context.Request.Headers.Add("Cookie", new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ // Missing chunk "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ });
+
+ Assert.Throws<FormatException>(() => new ChunkingCookieManager() { ThrowForPartialCookies = true }
+ .GetRequestCookie(context, "TestCookie"));
+ }
+
+ [Fact]
+ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassembled()
+ {
+ var context = new OwinContext();
+ context.Request.Headers.Add("Cookie", new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ // Missing chunk "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ });
+
+ string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie");
+ string testString = "chunks-7";
+ Assert.Equal(testString, result);
+ }
+
+ [Fact]
+ public void DeleteChunkedCookieWithOptions_AllDeleted()
+ {
+ var context = new OwinContext();
+ context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC3=3;TestCookieC4=4;TestCookieC5=5;TestCookieC6=6;TestCookieC7=7");
+
+ new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true, SameSite = SameSiteMode.Lax });
+ Assert.True(context.Response.Headers.TryGetValue("Set-Cookie", out var cookies));
+ Assert.Equal(8, cookies.Length);
+ Assert.Equal(new[]
+ {
+ "TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC3=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC4=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC5=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC6=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC7=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ }, cookies);
+ }
+
+ [Fact]
+ public void DeleteChunkedCookieWithMissingRequestCookies_OnlyPresentCookiesDeleted()
+ {
+ var context = new OwinContext();
+ context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2");
+
+ new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true, SameSite = SameSiteMode.Lax });
+ Assert.True(context.Response.Headers.TryGetValue("Set-Cookie", out var cookies));
+ Assert.Equal(3, cookies.Length);
+ Assert.Equal(new[]
+ {
+ "TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ }, cookies);
+ }
+
+ [Fact]
+ public void DeleteChunkedCookieWithMissingRequestCookies_StopsAtMissingChunk()
+ {
+ var context = new OwinContext();
+ // C3 is missing so we don't try to delete C4 either.
+ context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC4=4");
+
+ new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true, SameSite = SameSiteMode.Lax });
+ Assert.True(context.Response.Headers.TryGetValue("Set-Cookie", out var cookies));
+ Assert.Equal(3, cookies.Length);
+ Assert.Equal(new[]
+ {
+ "TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ }, cookies);
+ }
+
+ [Fact]
+ public void DeleteChunkedCookieWithOptionsAndResponseCookies_AllDeleted()
+ {
+ var chunkingCookieManager = new ChunkingCookieManager();
+ var context = new OwinContext();
+
+ context.Request.Headers.Add("Cookie", new[]
+ {
+ "TestCookie=chunks-7",
+ "TestCookieC1=abcdefghi",
+ "TestCookieC2=jklmnopqr",
+ "TestCookieC3=stuvwxyz0",
+ "TestCookieC4=123456789",
+ "TestCookieC5=ABCDEFGHI",
+ "TestCookieC6=JKLMNOPQR",
+ "TestCookieC7=STUVWXYZ"
+ });
+
+ var cookieOptions = new CookieOptions()
+ {
+ Domain = "foo.com",
+ Path = "/",
+ Secure = true,
+ SameSite = SameSiteMode.Lax
+ };
+
+ context.Response.Headers.Add("Set-Cookie", new[]
+ {
+ "TestCookie=chunks-7; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC1=STUVWXYZ; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC2=123456789; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC3=stuvwxyz0; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC4=123456789; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC5=ABCDEFGHI; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC6=JKLMNOPQR; domain=foo.com; path=/; secure; SameSite=Lax",
+ "TestCookieC7=STUVWXYZ; domain=foo.com; path=/; secure; SameSite=Lax"
+ });
+
+ chunkingCookieManager.DeleteCookie(context, "TestCookie", cookieOptions);
+ Assert.True(context.Response.Headers.TryGetValue("Set-Cookie", out var cookies));
+ Assert.Equal(8, cookies.Length);
+ Assert.Equal(new[]
+ {
+ "TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC3=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC4=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC5=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC6=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax",
+ "TestCookieC7=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT; secure; SameSite=Lax"
+ }, cookies);
+ }
+ }
+}
diff --git a/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs b/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs
index ba4fabc624..368abdabf4 100644
--- a/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs
+++ b/src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs
@@ -7,29 +7,32 @@ using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
+#if INTEROP
+using Microsoft.Owin.Infrastructure;
+#else
using Microsoft.Net.Http.Headers;
+#endif
// Keep the type public for Security repo as it would be a breaking change to change the accessor now.
// Make this type internal for other repos as it could be used by multiple projects and having it public causes type conflicts.
#if SECURITY
namespace Microsoft.AspNetCore.Authentication.Cookies
-{
- /// <summary>
- /// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
- /// from requests.
- /// </summary>
- public class ChunkingCookieManager : ICookieManager
- {
+#elif INTEROP
+namespace Microsoft.Owin.Security.Interop
#else
namespace Microsoft.AspNetCore.Internal
+#endif
{
/// <summary>
/// This handles cookies that are limited by per cookie length. It breaks down long cookies for responses, and reassembles them
/// from requests.
/// </summary>
+#if SECURITY || INTEROP
+ public class ChunkingCookieManager : ICookieManager
+#else
internal class ChunkingCookieManager
- {
#endif
+ {
/// <summary>
/// The default maximum size of characters in a cookie to send back to the client.
/// </summary>
@@ -82,7 +85,11 @@ namespace Microsoft.AspNetCore.Internal
/// <param name="context"></param>
/// <param name="key"></param>
/// <returns>The reassembled cookie, if any, or null.</returns>
+#if INTEROP
+ public string GetRequestCookie(IOwinContext context, string key)
+#else
public string GetRequestCookie(HttpContext context, string key)
+#endif
{
if (context == null)
{
@@ -144,7 +151,11 @@ namespace Microsoft.AspNetCore.Internal
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
+#if INTEROP
+ public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
+#else
public void AppendResponseCookie(HttpContext context, string key, string value, CookieOptions options)
+# endif
{
if (context == null)
{
@@ -161,6 +172,24 @@ namespace Microsoft.AspNetCore.Internal
throw new ArgumentNullException(nameof(options));
}
+#if INTEROP
+ var domainHasValue = !string.IsNullOrEmpty(options.Domain);
+ var pathHasValue = !string.IsNullOrEmpty(options.Path);
+ var expiresHasValue = options.Expires.HasValue;
+ var sameSiteLength = options.SameSite == SameSiteMode.None ? "; SameSite=None".Length
+ : options.SameSite == SameSiteMode.Lax ? "; SameSite=Lax".Length
+ : options.SameSite == SameSiteMode.Strict ? "; SameSite=Strict".Length
+ : 0;
+
+ var templateLength = key.Length + "=".Length
+ + (domainHasValue ? "; domain=".Length + options.Domain.Length : 0)
+ + (pathHasValue ? "; path=".Length + options.Path.Length : 0)
+ + (expiresHasValue ? "; expires=ddd, dd-MMM-yyyy HH:mm:ss GMT".Length : 0)
+ + sameSiteLength
+ + (options.Secure ? "; secure".Length : 0)
+ + (options.HttpOnly ? "; HttpOnly".Length : 0);
+#else
+
var template = new SetCookieHeaderValue(key)
{
Domain = options.Domain,
@@ -173,6 +202,7 @@ namespace Microsoft.AspNetCore.Internal
};
var templateLength = template.ToString().Length;
+#endif
value = value ?? string.Empty;
@@ -221,7 +251,11 @@ namespace Microsoft.AspNetCore.Internal
/// <param name="context"></param>
/// <param name="key"></param>
/// <param name="options"></param>
+#if INTEROP
+ public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
+#else
public void DeleteCookie(HttpContext context, string key, CookieOptions options)
+#endif
{
if (context == null)
{
@@ -280,11 +314,18 @@ namespace Microsoft.AspNetCore.Internal
}
var responseHeaders = context.Response.Headers;
+#if INTEROP
+ if (responseHeaders.TryGetValue(Constants.Headers.SetCookie, out var existingValues) && existingValues != null)
+ {
+ responseHeaders.SetValues(Constants.Headers.SetCookie, existingValues.Where(value => !rejectPredicate(value)).ToArray());
+ }
+#else
var existingValues = responseHeaders[HeaderNames.SetCookie];
if (!StringValues.IsNullOrEmpty(existingValues))
{
responseHeaders[HeaderNames.SetCookie] = existingValues.Where(value => !rejectPredicate(value)).ToArray();
}
+#endif
AppendResponseCookie(
context,
@@ -296,7 +337,9 @@ namespace Microsoft.AspNetCore.Internal
Domain = options.Domain,
SameSite = options.SameSite,
Secure = options.Secure,
+#if !INTEROP
IsEssential = options.IsEssential,
+#endif
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
HttpOnly = options.HttpOnly,
});
@@ -313,7 +356,9 @@ namespace Microsoft.AspNetCore.Internal
Domain = options.Domain,
SameSite = options.SameSite,
Secure = options.Secure,
+#if !INTEROP
IsEssential = options.IsEssential,
+#endif
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
HttpOnly = options.HttpOnly,
});
diff --git a/version.props b/version.props
index 27905279d9..5732b372dc 100644
--- a/version.props
+++ b/version.props
@@ -2,8 +2,8 @@
<PropertyGroup>
<AspNetCoreMajorVersion>2</AspNetCoreMajorVersion>
<AspNetCoreMinorVersion>1</AspNetCoreMinorVersion>
- <AspNetCorePatchVersion>38</AspNetCorePatchVersion>
- <ValidateBaseline>false</ValidateBaseline>
+ <AspNetCorePatchVersion>39</AspNetCorePatchVersion>
+ <ValidateBaseline>true</ValidateBaseline>
<PreReleaseLabel>servicing</PreReleaseLabel>
<PreReleaseBrandingLabel>Servicing</PreReleaseBrandingLabel>