diff --git a/QueryKit.UnitTests/ParseLimitsTests.cs b/QueryKit.UnitTests/ParseLimitsTests.cs index 916eec9..7d9b12a 100644 --- a/QueryKit.UnitTests/ParseLimitsTests.cs +++ b/QueryKit.UnitTests/ParseLimitsTests.cs @@ -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(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(input); - filterExpression.Should().NotBeNull(); + var act = () => FilterParser.ParseFilter(input); + act.Should().Throw() + .WithMessage($"*depth of {QueryKitSettings.DefaultMaxNestingDepth + 1}*maximum allowed depth of {QueryKitSettings.DefaultMaxNestingDepth}*"); } [Fact] @@ -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(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(input); + act.Should().Throw() + .WithMessage($"*length of {input.Length}*maximum allowed length of {QueryKitSettings.DefaultMaxInputLength}*"); + } + [Fact] public void filter_over_configured_input_length_throws() { @@ -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(deep, config).Should().NotBeNull(); + var filterExpression = FilterParser.ParseFilter("""Title == "salt" """, config); + filterExpression.Should().NotBeNull(); - var longInput = $"""Title == "{new string('a', 5000)}" """; - FilterParser.ParseFilter(longInput, config).Should().NotBeNull(); + var tooDeep = new string('(', QueryKitSettings.DefaultMaxNestingDepth + 1) + + """Title == "salt" """ + + new string(')', QueryKitSettings.DefaultMaxNestingDepth + 1); + var actDeep = () => FilterParser.ParseFilter(tooDeep, config); + actDeep.Should().Throw() + .WithMessage($"*maximum allowed depth of {QueryKitSettings.DefaultMaxNestingDepth}*"); + + var tooLong = $"""Title == "{new string('a', QueryKitSettings.DefaultMaxInputLength)}" """; + var actLong = () => FilterParser.ParseFilter(tooLong, config); + actLong.Should().Throw() + .WithMessage($"*maximum allowed length of {QueryKitSettings.DefaultMaxInputLength}*"); } [Fact] @@ -217,7 +230,11 @@ public void deep_filter_with_quoted_close_parentheses_throws_instead_of_overflow { try { - FilterParser.ParseFilter(input, DepthLimit(10)); + FilterParser.ParseFilter(input, new QueryKitConfiguration(settings => + { + settings.MaxNestingDepth = 10; + settings.MaxInputLength = int.MaxValue; + })); } catch (Exception e) { diff --git a/QueryKit/Configuration/QueryKitSettings.cs b/QueryKit/Configuration/QueryKitSettings.cs index 6e64c92..0d4fe3f 100644 --- a/QueryKit/Configuration/QueryKitSettings.cs +++ b/QueryKit/Configuration/QueryKitSettings.cs @@ -6,14 +6,14 @@ namespace QueryKit.Configuration; public class QueryKitSettings { /// - /// The default nesting depth limit is off. Set to turn the limit on. + /// The default nesting depth limit. Set to int.MaxValue to turn the limit off. /// - public const int DefaultMaxNestingDepth = int.MaxValue; + public const int DefaultMaxNestingDepth = 32; /// - /// The default input length limit is off. Set to turn the limit on. + /// The default input length limit. Set to int.MaxValue to turn the limit off. /// - 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(); diff --git a/README.md b/README.md index 9bc8267..d5324de 100644 --- a/README.md +++ b/README.md @@ -827,7 +827,7 @@ var filterExpression = FilterParser.ParseFilter(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 =>