Skip to content
Open
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
2 changes: 2 additions & 0 deletions QueryKit.UnitTests/FilterBehaviorInterfaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ internal class InterfaceOnlyConfiguration : IQueryKitConfiguration, IQueryKitFil
public string HasOperator { get; set; } = "^$";
public string DoesNotHaveOperator { get; set; } = "!^$";
public int? MaxPropertyDepth { get; set; }
public int MaxNestingDepth { get; set; } = QueryKitSettings.DefaultMaxNestingDepth;
public int MaxInputLength { get; set; } = QueryKitSettings.DefaultMaxInputLength;
public CaseInsensitiveMode CaseInsensitiveComparison { get; set; } = CaseInsensitiveMode.Lower;
public bool ParameterizeFilterValues { get; set; }
public IgnoredClauseBehavior IgnoredClauseBehavior { get; set; }
Expand Down
33 changes: 13 additions & 20 deletions QueryKit.UnitTests/ParseLimitsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,9 @@ 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_the_interface_uses_its_own_limits()
{
var config = new FilterBehaviorInterfaceTests.InterfaceOnlyConfiguration();

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

var longInput = $"""Title == "{new string('a', 5000)}" """;
FilterParser.ParseFilter<TestingPerson>(longInput, config).Should().NotBeNull();
}

[Fact]
public void configuration_that_implements_the_parse_limits_uses_its_own_limits()
{
var config = new InterfaceOnlyConfigurationWithLimits { MaxNestingDepth = 2, MaxInputLength = 100 };
var config = new FilterBehaviorInterfaceTests.InterfaceOnlyConfiguration { MaxNestingDepth = 2, MaxInputLength = 100 };
var input = new string('(', 3) + """Title == "salt" """ + new string(')', 3);

var act = () => FilterParser.ParseFilter<TestingPerson>(input, config);
Expand Down Expand Up @@ -231,12 +219,17 @@ public void deep_filter_with_quoted_close_parentheses_throws_instead_of_overflow
.Which.Message.Should().Contain("depth of 11");
}

private static QueryKitConfiguration DepthLimit(int maxNestingDepth)
=> new(settings => settings.MaxNestingDepth = maxNestingDepth);

private sealed class InterfaceOnlyConfigurationWithLimits : FilterBehaviorInterfaceTests.InterfaceOnlyConfiguration, IQueryKitParseLimits
[Fact]
public void configuration_that_implements_the_interface_uses_its_own_input_length()
{
public int MaxNestingDepth { get; set; }
public int MaxInputLength { get; set; }
var config = new FilterBehaviorInterfaceTests.InterfaceOnlyConfiguration { MaxNestingDepth = 32, MaxInputLength = 10 };
var input = """Title == "salt and pepper" """;

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

private static QueryKitConfiguration DepthLimit(int maxNestingDepth)
=> new(settings => settings.MaxNestingDepth = maxNestingDepth);
}
12 changes: 0 additions & 12 deletions QueryKit/Configuration/IQueryKitParseLimits.cs

This file was deleted.

4 changes: 3 additions & 1 deletion QueryKit/Configuration/QueryKitConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ public interface IQueryKitConfiguration
public string HasOperator { get; set; }
public string DoesNotHaveOperator { get; set; }
public int? MaxPropertyDepth { get; set; }
public int MaxNestingDepth { get; set; }
public int MaxInputLength { get; set; }
public CaseInsensitiveMode CaseInsensitiveComparison { get; set; }
}

public class QueryKitConfiguration : IQueryKitConfiguration, IQueryKitParseLimits, IQueryKitFilterBehavior
public class QueryKitConfiguration : IQueryKitConfiguration, IQueryKitFilterBehavior
{
public QueryKitPropertyMappings PropertyMappings { get; }
public string EqualsOperator { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions QueryKit/FilterParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static Expression<Func<T, bool>> ParseFilter<T>(string input, IQueryKitCo
var nestingDepthBefore = _nestingDepth;
var queryNameOverUnknownBefore = _queryNameOverUnknown;
var queryNameFallbackOffBefore = _queryNameFallbackOff;
_maxNestingDepth = (config as IQueryKitParseLimits)?.MaxNestingDepth ?? QueryKitSettings.DefaultMaxNestingDepth;
_maxNestingDepth = config?.MaxNestingDepth ?? QueryKitSettings.DefaultMaxNestingDepth;
_nestingDepth = 0;
_queryNameOverUnknown = false;
_queryNameFallbackOff = false;
Expand Down Expand Up @@ -95,7 +95,7 @@ private static Expression ReplaceDerivedProperties(Expression expr, IQueryKitCon
// QueryKitException instead of exhausting CPU and memory during parsing.
private static void EnsureWithinInputLength(string input, IQueryKitConfiguration? config)
{
var maxLength = (config as IQueryKitParseLimits)?.MaxInputLength ?? QueryKitSettings.DefaultMaxInputLength;
var maxLength = config?.MaxInputLength ?? QueryKitSettings.DefaultMaxInputLength;
if (input.Length > maxLength)
{
throw new QueryKitInputLengthExceededException(input.Length, maxLength);
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,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.
`IQueryKitConfiguration` 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. A custom class that implements `IQueryKitConfiguration` must give a value for both limits. Use `int.MaxValue` to turn a limit off, because a limit of 0 rejects every filter. 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.

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