From 404ef8c1a1697d62a6c261005696b7d5f05e8466 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Wed, 30 Sep 2026 17:02:22 +0300 Subject: [PATCH] feat(config)!: put the parse limits on IQueryKitConfiguration MaxNestingDepth and MaxInputLength move from IQueryKitParseLimits to IQueryKitConfiguration. Every configuration now gives its parse limits in one place, and the parser reads them from any configuration that is not null. IQueryKitParseLimits is removed. BREAKING CHANGE: A class that implements IQueryKitConfiguration directly must implement MaxNestingDepth and MaxInputLength. A library built against v1.14.2 fails with TypeLoadException. Set real values, because a limit of 0 rejects every filter. The interface IQueryKitParseLimits is removed. --- .../FilterBehaviorInterfaceTests.cs | 2 ++ QueryKit.UnitTests/ParseLimitsTests.cs | 33 ++++++++----------- .../Configuration/IQueryKitParseLimits.cs | 12 ------- .../Configuration/QueryKitConfiguration.cs | 4 ++- QueryKit/FilterParser.cs | 4 +-- README.md | 2 +- 6 files changed, 21 insertions(+), 36 deletions(-) delete mode 100644 QueryKit/Configuration/IQueryKitParseLimits.cs diff --git a/QueryKit.UnitTests/FilterBehaviorInterfaceTests.cs b/QueryKit.UnitTests/FilterBehaviorInterfaceTests.cs index e0eb581..ec17633 100644 --- a/QueryKit.UnitTests/FilterBehaviorInterfaceTests.cs +++ b/QueryKit.UnitTests/FilterBehaviorInterfaceTests.cs @@ -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; } diff --git a/QueryKit.UnitTests/ParseLimitsTests.cs b/QueryKit.UnitTests/ParseLimitsTests.cs index 916eec9..3e1d69f 100644 --- a/QueryKit.UnitTests/ParseLimitsTests.cs +++ b/QueryKit.UnitTests/ParseLimitsTests.cs @@ -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(deep, config).Should().NotBeNull(); - - var longInput = $"""Title == "{new string('a', 5000)}" """; - FilterParser.ParseFilter(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(input, config); @@ -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(input, config); + act.Should().Throw() + .WithMessage($"*length of {input.Length}*maximum allowed length of 10*"); } + + private static QueryKitConfiguration DepthLimit(int maxNestingDepth) + => new(settings => settings.MaxNestingDepth = maxNestingDepth); } diff --git a/QueryKit/Configuration/IQueryKitParseLimits.cs b/QueryKit/Configuration/IQueryKitParseLimits.cs deleted file mode 100644 index 563f243..0000000 --- a/QueryKit/Configuration/IQueryKitParseLimits.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace QueryKit.Configuration; - -/// -/// The limits that the filter parser applies to a filter. A configuration that does not -/// implement this interface uses and -/// . -/// -public interface IQueryKitParseLimits -{ - int MaxNestingDepth { get; } - int MaxInputLength { get; } -} diff --git a/QueryKit/Configuration/QueryKitConfiguration.cs b/QueryKit/Configuration/QueryKitConfiguration.cs index 55cec7e..9fb31f1 100644 --- a/QueryKit/Configuration/QueryKitConfiguration.cs +++ b/QueryKit/Configuration/QueryKitConfiguration.cs @@ -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; } diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 07ead9d..c086bfd 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -34,7 +34,7 @@ public static Expression> ParseFilter(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; @@ -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); diff --git a/README.md b/README.md index 96f5341..eeb1264 100644 --- a/README.md +++ b/README.md @@ -823,7 +823,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. +`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 =>