Skip to content
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
61 changes: 39 additions & 22 deletions QueryKit.UnitTests/ParseLimitsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,15 @@ public void filter_within_default_nesting_depth_parses()
}

[Fact]
public void filter_over_33_nesting_levels_parses_by_default()
public void filter_over_default_nesting_depth_throws()
{
var input = new string('(', 33) + """Title == "salt" """ + new string(')', 33);
var input = new string('(', QueryKitSettings.DefaultMaxNestingDepth + 1)
+ """Title == "salt" """
+ new string(')', QueryKitSettings.DefaultMaxNestingDepth + 1);

var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.Should().NotBeNull();
}

[Fact]
public void quoted_value_with_33_parentheses_parses_by_default()
{
var input = $"""Title == "{new string('(', 33)}" """;

var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.Should().NotBeNull();
var act = () => FilterParser.ParseFilter<TestingPerson>(input);
act.Should().Throw<QueryKitNestingDepthExceededException>()
.WithMessage($"*depth of {QueryKitSettings.DefaultMaxNestingDepth + 1}*maximum allowed depth of {QueryKitSettings.DefaultMaxNestingDepth}*");
}

[Fact]
Expand Down Expand Up @@ -71,15 +65,25 @@ public void filter_within_default_input_length_parses()
}

[Fact]
public void filter_over_5000_characters_parses_by_default()
public void quoted_value_with_33_parentheses_parses_by_default()
{
var padding = new string('a', 5000);
var input = $"""Title == "{padding}" """;
var input = $"""Title == "{new string('(', 33)}" """;

var filterExpression = FilterParser.ParseFilter<TestingPerson>(input);
filterExpression.Should().NotBeNull();
}

[Fact]
public void filter_over_default_input_length_throws()
{
var padding = new string('a', QueryKitSettings.DefaultMaxInputLength);
var input = $"""Title == "{padding}" """;

var act = () => FilterParser.ParseFilter<TestingPerson>(input);
act.Should().Throw<QueryKitInputLengthExceededException>()
.WithMessage($"*length of {input.Length}*maximum allowed length of {QueryKitSettings.DefaultMaxInputLength}*");
}

[Fact]
public void filter_over_configured_input_length_throws()
{
Expand Down Expand Up @@ -108,15 +112,24 @@ public void filter_within_configured_input_length_parses()
}

[Fact]
public void configuration_that_implements_only_the_interface_has_no_limits()
public void configuration_that_implements_only_the_interface_uses_the_default_limits()
{
var config = new FilterBehaviorInterfaceTests.InterfaceOnlyConfiguration();

var deep = new string('(', 33) + """Title == "salt" """ + new string(')', 33);
FilterParser.ParseFilter<TestingPerson>(deep, config).Should().NotBeNull();
var filterExpression = FilterParser.ParseFilter<TestingPerson>("""Title == "salt" """, config);
filterExpression.Should().NotBeNull();

var longInput = $"""Title == "{new string('a', 5000)}" """;
FilterParser.ParseFilter<TestingPerson>(longInput, config).Should().NotBeNull();
var tooDeep = new string('(', QueryKitSettings.DefaultMaxNestingDepth + 1)
+ """Title == "salt" """
+ new string(')', QueryKitSettings.DefaultMaxNestingDepth + 1);
var actDeep = () => FilterParser.ParseFilter<TestingPerson>(tooDeep, config);
actDeep.Should().Throw<QueryKitNestingDepthExceededException>()
.WithMessage($"*maximum allowed depth of {QueryKitSettings.DefaultMaxNestingDepth}*");

var tooLong = $"""Title == "{new string('a', QueryKitSettings.DefaultMaxInputLength)}" """;
var actLong = () => FilterParser.ParseFilter<TestingPerson>(tooLong, config);
actLong.Should().Throw<QueryKitInputLengthExceededException>()
.WithMessage($"*maximum allowed length of {QueryKitSettings.DefaultMaxInputLength}*");
}

[Fact]
Expand Down Expand Up @@ -217,7 +230,11 @@ public void deep_filter_with_quoted_close_parentheses_throws_instead_of_overflow
{
try
{
FilterParser.ParseFilter<TestingPerson>(input, DepthLimit(10));
FilterParser.ParseFilter<TestingPerson>(input, new QueryKitConfiguration(settings =>
{
settings.MaxNestingDepth = 10;
settings.MaxInputLength = int.MaxValue;
}));
}
catch (Exception e)
{
Expand Down
8 changes: 4 additions & 4 deletions QueryKit/Configuration/QueryKitSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ namespace QueryKit.Configuration;
public class QueryKitSettings
{
/// <summary>
/// The default nesting depth limit is off. Set <see cref="MaxNestingDepth"/> to turn the limit on.
/// The default nesting depth limit. Set <see cref="MaxNestingDepth"/> to int.MaxValue to turn the limit off.
/// </summary>
public const int DefaultMaxNestingDepth = int.MaxValue;
public const int DefaultMaxNestingDepth = 32;

/// <summary>
/// The default input length limit is off. Set <see cref="MaxInputLength"/> to turn the limit on.
/// The default input length limit. Set <see cref="MaxInputLength"/> to int.MaxValue to turn the limit off.
/// </summary>
public const int DefaultMaxInputLength = int.MaxValue;
public const int DefaultMaxInputLength = 5000;

public QueryKitPropertyMappings PropertyMappings { get; set; } = new QueryKitPropertyMappings();
public string EqualsOperator { get; set; } = ComparisonOperator.EqualsOperator().Operator();
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ var filterExpression = FilterParser.ParseFilter<Recipe>(input, config);

#### Parse Limits

`IQueryKitParseLimits` caps how much a filter string can do, through `MaxInputLength` (a number of characters) and `MaxNestingDepth` (a number of levels of parentheses). Both limits are off by default. If your app sends user input to QueryKit, turn both limits on. A filter string with a few thousand nested parentheses can overflow the call stack and stop the process. `QueryKitConfiguration` implements this interface, so a custom configuration class can implement `IQueryKitParseLimits` directly instead. A filter string that goes over `MaxInputLength` throws a `QueryKitInputLengthExceededException` before parsing starts. A filter string that goes over `MaxNestingDepth` throws a `QueryKitNestingDepthExceededException` when the parser enters the group that goes over the limit. These limits apply only to filter strings. Sort strings have no limit, and the number of items in an in-list has no limit. The nesting depth counts each parenthesized group in the filter: a logical group, an arithmetic group, and a property list. A `(` or `)` inside a quoted value is part of the value, so it does not change the depth. `MaxNestingDepth` does not limit a long flat chain of `&&` or `||` clauses, and a very long chain can also overflow the call stack. Keep `MaxInputLength` small to limit such a chain.
`IQueryKitParseLimits` caps how much a filter string can do, through `MaxInputLength` (default `5000` characters) and `MaxNestingDepth` (default `32` levels of parentheses). To turn a limit off, set it to `int.MaxValue`. A filter string with a few thousand nested parentheses can overflow the call stack and stop the process. `QueryKitConfiguration` implements this interface, so a custom configuration class can implement `IQueryKitParseLimits` directly instead. A filter string that goes over `MaxInputLength` throws a `QueryKitInputLengthExceededException` before parsing starts. A filter string that goes over `MaxNestingDepth` throws a `QueryKitNestingDepthExceededException` when the parser enters the group that goes over the limit. These limits apply only to filter strings. Sort strings have no limit, and the number of items in an in-list has no limit. The nesting depth counts each parenthesized group in the filter: a logical group, an arithmetic group, and a property list. A `(` or `)` inside a quoted value is part of the value, so it does not change the depth. `MaxNestingDepth` does not limit a long flat chain of `&&` or `||` clauses, and a very long chain can also overflow the call stack. Keep `MaxInputLength` small to limit such a chain. The default of `5000` characters fits an in-list of about 127 GUIDs. It is the only limit on a flat chain, and a flat `&&` chain of about 10,000 characters overflowed a 1 MB stack, so raise the limit with care.

```csharp
var config = new QueryKitConfiguration(config =>
Expand Down
Loading