Skip to content

ModelValidation: Fixed MaxDepth exception when there are more items left then MaxValidationDepth after reaching MaxAllowedErrors #18212

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/Mvc/Mvc.Core/src/ModelBinding/Validation/ValidationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,25 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
return true;
}

bool result;
try
{
// Throws InvalidOperationException if the object graph is too deep
result = VisitImplementation(ref metadata, ref key, model);
}
finally
{
_currentPath.Pop(model);
}
return result;
}

private bool VisitImplementation(ref ModelMetadata metadata, ref string key, object model)
{
if (MaxValidationDepth != null && _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)
{
Expand Down Expand Up @@ -264,7 +276,6 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
{
// Use the key on the entry, because we might not have entries in model state.
SuppressValidation(entry.Key);
_currentPath.Pop(model);
return true;
}
// If the metadata indicates that no validators exist AND the aggregate state for the key says that the model graph
Expand All @@ -282,7 +293,6 @@ protected virtual bool Visit(ModelMetadata metadata, string key, object model)
}
}

_currentPath.Pop(model);
return true;
}

Expand Down Expand Up @@ -401,7 +411,6 @@ protected virtual ValidationStateEntry GetValidationEntry(object model)
private readonly string _key;
private readonly ModelMetadata _metadata;
private readonly object _model;
private readonly object _newModel;
private readonly IValidationStrategy _strategy;

public static StateManager Recurse(
Expand All @@ -411,7 +420,7 @@ public static StateManager Recurse(
object model,
IValidationStrategy strategy)
{
var recursifier = new StateManager(visitor, model);
var recursifier = new StateManager(visitor, null);

visitor.Container = visitor.Model;
visitor.Key = key;
Expand All @@ -425,7 +434,6 @@ public static StateManager Recurse(
public StateManager(ValidationVisitor visitor, object newModel)
{
_visitor = visitor;
_newModel = newModel;

_container = _visitor.Container;
_key = _visitor.Key;
Expand All @@ -441,8 +449,6 @@ public void Dispose()
_visitor.Metadata = _metadata;
_visitor.Model = _model;
_visitor.Strategy = _strategy;

_visitor._currentPath.Pop(_newModel);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part seems like a big red flag (in the existing code, not in your changes). Seeing pushes and pops so disconnected from each other is a good clue that we had a bug here, your changes make this much easier to reason about 👍

}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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;
Expand Down Expand Up @@ -1264,11 +1263,12 @@ public void Validate_SuppressValidation_AfterHasReachedMaxErrors_Invalid()
});
}

[Fact]
public void Validate_Throws_IfValidationDepthExceedsMaxDepth()
[Theory]
[InlineData(1)]
[InlineData(5)]
public void Validate_Throws_IfValidationDepthExceedsMaxDepth(int maxDepth)
{
// 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 modifying 'MvcOptions.MaxValidationDepth' or suppressing validation on the model type.";
_options.MaxValidationDepth = maxDepth;
Expand All @@ -1283,6 +1283,7 @@ public void Validate_Throws_IfValidationDepthExceedsMaxDepth()
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
Assert.Equal(expected, ex.Message);
Assert.Equal("https://aka.ms/AA21ue1", ex.HelpLink);
}

[Fact]
Expand All @@ -1305,26 +1306,32 @@ public void Validate_WorksIfObjectGraphIsSmallerThanMaxDepth()
}

[Fact]
public void Validate_Throws_WithMaxDepth_1()
public void Validate_DoesNotThrow_IfNumberOfErrorsAfterReachingMaxAllowedErrors_GoesOverMaxDepth()
{
// 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 modifying 'MvcOptions.MaxValidationDepth' or suppressing validation on the model type.";
var maxDepth = 4;
_options.MaxValidationDepth = maxDepth;
var actionContext = new ActionContext();
actionContext.ModelState.MaxAllowedErrors = 2;
var validator = CreateValidator();
var model = new DepthObject(maxDepth + 1);
var model = new List<ModelWithRequiredProperty>
{
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
// After the first 2 items we will reach MaxAllowedErrors
// If we add items without popping after having reached max validation,
// with 4 more items (on top of the list) we would go over max depth of 4
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
new ModelWithRequiredProperty(), new ModelWithRequiredProperty(),
};

var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry() }
};
var method = GetType().GetMethod(nameof(Validate_Throws_ForTopLevelMetadataData), BindingFlags.NonPublic | BindingFlags.Instance);

// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => validator.Validate(actionContext, validationState, prefix: string.Empty, model));
Assert.Equal(expected, ex.Message);
Assert.NotNull(ex.HelpLink);
validator.Validate(actionContext, validationState, prefix: string.Empty, model);
Assert.False(actionContext.ModelState.IsValid);
}

[Theory]
Expand Down Expand Up @@ -1443,6 +1450,12 @@ public void Validate_TypeWithoutValidators_DoesNotResetInvalidState()
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
}

public class ModelWithRequiredProperty
{
[Required]
public string MyProperty { get; set; }
}

private class ModelWithoutValidation
{
public string Property1 { get; set; }
Expand Down