From 59a8cb205c049e72e0a932d48aaeeec20ef701d1 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 12 Nov 2026 18:30:39 +0100 Subject: [PATCH 1/4] Add tests for dictionary method accessibility validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../QueryableTests.Where.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index f5300898..faa08a08 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -162,6 +162,69 @@ public void Where_Dynamic_Exceptions() act.Should().Throw().WithMessage("The ParsingConfig should be provided as first argument to this method.*"); } + public class DictionaryValidationService + { + public bool Danger() => true; + } + + public class CustomDictionary : Dictionary, IDictionary + { + public int CallCount { get; private set; } + + public bool Danger() + { + CallCount++; + return true; + } + + public void Reset(string key) + { + CallCount++; + this[key] = "reset"; + } + } + + public class DictionaryValidationItem + { + public CustomDictionary Custom { get; set; } = new() { { "k", "v" } }; + + public string Name { get; set; } = null!; + + public DictionaryValidationService S { get; set; } = new(); + + public Dictionary Meta { get; set; } = new() { { "k", "v" } }; + + public System.Collections.Concurrent.ConcurrentDictionary C { get; set; } = new(); + } + + [Theory] + [InlineData("S.Danger()")] + [InlineData("Meta.TryAdd(\"z\",\"y\")")] + [InlineData("Meta.EnsureCapacity(100000000) > 0")] + [InlineData("C.TryAdd(\"x\",\"y\")")] + [InlineData("C.TryUpdate(\"x\",\"z\",\"y\")")] + [InlineData("Custom.Danger()")] + [InlineData("Custom.Reset(\"k\")")] + public void Where_Dynamic_DictionaryMethods_ShouldThrowParseException_WhenMethodIsNotAccessible(string query) + { + // Arrange + var items = new List + { + new() { Name = "a" }, + new() { Name = "b" } + }.AsQueryable(); + + // Act + Action act = () => items.Where(query).ToList(); + + // Assert + act.Should().Throw().WithMessage("Method '*' on type '*' is not accessible*"); + items.Select(i => i.Meta.Count).Should().AllBeEquivalentTo(1); + items.Select(i => i.C.Count).Should().AllBeEquivalentTo(0); + items.Select(i => i.Custom.CallCount).Should().AllBeEquivalentTo(0); + items.Select(i => i.Custom["k"]).Should().AllBeEquivalentTo("v"); + } + [Fact] public void Where_Dynamic_StringQuoted() { From 03416dfca1a8780b1f30a6e921ad5ea2a89fb356 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 12 Nov 2026 19:05:26 +0100 Subject: [PATCH 2/4] Validate dictionary method calls using PredefinedMethodsHelper Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs | 5 +++++ .../Parser/PredefinedMethodsHelper.cs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index add680a5..c489f26c 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2324,6 +2324,11 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string if (type != null && TypeHelper.IsDictionary(type) && _methodFinder.ContainsMethod(type, methodName, false)) { var dictionaryMethod = type.GetMethod(methodName)!; + if (!PredefinedMethodsHelper.IsPredefinedDictionaryMethod(dictionaryMethod)) + { + throw ParseError(tokenPos, Res.MethodIsInaccessible, methodName, TypeHelper.GetTypeName(dictionaryMethod.DeclaringType!)); + } + expression = Expression.Call(instance, dictionaryMethod, args); return true; } diff --git a/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs index a3b6d85e..e698a5f4 100644 --- a/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs @@ -20,6 +20,15 @@ internal static class PredefinedMethodsHelper ObjectStaticReferenceEquals ]; + private static readonly HashSet AllowedDictionaryMethodNames = ["ContainsKey", "ContainsValue", "TryGetValue", "get_Item"]; + + public static bool IsPredefinedDictionaryMethod(MethodInfo method) + { + Check.NotNull(method); + + return AllowedDictionaryMethodNames.Contains(method.Name); + } + public static bool IsPredefinedMethod(ParsingConfig config, MemberInfo member) { Check.NotNull(config); From 6a812bdd236bc0b496541674923c0f880b0c7e7c Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 12 Nov 2026 19:41:02 +0100 Subject: [PATCH 3/4] fix test --- .../QueryableTests.Where.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index faa08a08..35b83c39 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -219,10 +219,10 @@ public void Where_Dynamic_DictionaryMethods_ShouldThrowParseException_WhenMethod // Assert act.Should().Throw().WithMessage("Method '*' on type '*' is not accessible*"); - items.Select(i => i.Meta.Count).Should().AllBeEquivalentTo(1); - items.Select(i => i.C.Count).Should().AllBeEquivalentTo(0); - items.Select(i => i.Custom.CallCount).Should().AllBeEquivalentTo(0); - items.Select(i => i.Custom["k"]).Should().AllBeEquivalentTo("v"); + items.Select(i => i.Meta.Count).Should().OnlyContain(count => count == 1); + items.Select(i => i.C.Count).Should().OnlyContain(count => count == 0); + items.Select(i => i.Custom.CallCount).Should().OnlyContain(callCount => callCount == 0); + items.Select(i => i.Custom["k"]).Should().OnlyContain(value => value == "v"); } [Fact] From e7facf11cd2055db45a93412e4dc30076f2e3697 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 12 Nov 2026 19:57:05 +0100 Subject: [PATCH 4/4] more fixes --- README.md | 8 ++++ .../Parser/ExpressionParser.cs | 8 ++-- .../Parser/PredefinedMethodsHelper.cs | 31 +++++++++++-- .../QueryableTests.Where.cs | 45 ++++++++++++++++++- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7fc38608..4666eb73 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,14 @@ The `DefaultDynamicLinqCustomTypeProvider` has been changed to only return types If it's not possible to add that attribute, you need to implement a custom [CustomTypeProvider](https://dynamic-linq.net/advanced-configuration#customtypeprovider) and set this to the `ParsingConfig` and provide that config to all dynamic calls. Or provide a list of additional types in the [DefaultDynamicLinqCustomTypeProvider.cs](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs). +### v1.7.5 + +#### Dictionary methods +Dictionary method calls are restricted to recognized framework dictionary read methods. +Custom dictionary methods, including methods that hide or overload those names, are not permitted through this path. +Dictionary interface calls and other virtual calls can still dispatch to application-provided implementations, and enumerable operations can execute application-provided enumerators. +Use trusted implementations when evaluating untrusted expressions. + --- ## Useful links diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c489f26c..175e8f4d 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2323,12 +2323,14 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string if (type != null && TypeHelper.IsDictionary(type) && _methodFinder.ContainsMethod(type, methodName, false)) { - var dictionaryMethod = type.GetMethod(methodName)!; - if (!PredefinedMethodsHelper.IsPredefinedDictionaryMethod(dictionaryMethod)) + var declaredMethods = type.GetMethods().Where(m => + m.DeclaringType == type && m.IsPublic && !m.IsStatic && m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase)); + if (declaredMethods.Any(m => !PredefinedMethodsHelper.IsPredefinedDictionaryMethod(m))) { - throw ParseError(tokenPos, Res.MethodIsInaccessible, methodName, TypeHelper.GetTypeName(dictionaryMethod.DeclaringType!)); + throw ParseError(tokenPos, Res.MethodIsInaccessible, methodName, TypeHelper.GetTypeName(type)); } + var dictionaryMethod = type.GetMethod(methodName)!; expression = Expression.Call(instance, dictionaryMethod, args); return true; } diff --git a/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs index e698a5f4..d14a4f4e 100644 --- a/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; +#if !(NET35 || NET40) +using System.Collections.ObjectModel; +#endif using System.Linq.Dynamic.Core.Validation; using System.Reflection; @@ -11,6 +15,13 @@ internal static class PredefinedMethodsHelper internal static readonly MethodInfo ObjectStaticEquals = typeof(object).GetMethod(nameof(Equals), BindingFlags.Static | BindingFlags.Public, null, [typeof(object), typeof(object)], null)!; internal static readonly MethodInfo ObjectStaticReferenceEquals = typeof(object).GetMethod(nameof(ReferenceEquals), BindingFlags.Static | BindingFlags.Public, null, [typeof(object), typeof(object)], null)!; + private static readonly HashSet AllowedDictionaryMethodNames = + [ + "ContainsKey", + "ContainsValue", + "TryGetValue", + "get_Item" + ]; private static readonly HashSet ObjectToStringAndObjectEquals = [ @@ -20,13 +31,25 @@ internal static class PredefinedMethodsHelper ObjectStaticReferenceEquals ]; - private static readonly HashSet AllowedDictionaryMethodNames = ["ContainsKey", "ContainsValue", "TryGetValue", "get_Item"]; - public static bool IsPredefinedDictionaryMethod(MethodInfo method) { Check.NotNull(method); - return AllowedDictionaryMethodNames.Contains(method.Name); + var declaringType = method.DeclaringType; + if (!AllowedDictionaryMethodNames.Contains(method.Name) || declaringType == null || !declaringType.GetTypeInfo().IsGenericType) + { + return false; + } + + var genericType = declaringType.GetGenericTypeDefinition(); + return genericType == typeof(Dictionary<,>) || + genericType == typeof(ConcurrentDictionary<,>) || + genericType == typeof(IDictionary<,>) +#if !(NET35 || NET40) + || genericType == typeof(IReadOnlyDictionary<,>) || + genericType == typeof(ReadOnlyDictionary<,>) +#endif + ; } public static bool IsPredefinedMethod(ParsingConfig config, MemberInfo member) diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 35b83c39..549652d5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; @@ -182,6 +183,26 @@ public void Reset(string key) CallCount++; this[key] = "reset"; } + + public new bool ContainsKey(string key) + { + CallCount++; + this[key] = "changed"; + return true; + } + + public bool ContainsValue(int value) + { + CallCount++; + return true; + } + + public new bool TryGetValue(string key, out string value) + { + CallCount++; + value = "changed"; + return true; + } } public class DictionaryValidationItem @@ -194,7 +215,7 @@ public class DictionaryValidationItem public Dictionary Meta { get; set; } = new() { { "k", "v" } }; - public System.Collections.Concurrent.ConcurrentDictionary C { get; set; } = new(); + public ConcurrentDictionary C { get; set; } = new(); } [Theory] @@ -205,6 +226,9 @@ public class DictionaryValidationItem [InlineData("C.TryUpdate(\"x\",\"z\",\"y\")")] [InlineData("Custom.Danger()")] [InlineData("Custom.Reset(\"k\")")] + [InlineData("Custom.ContainsKey(\"k\")")] + [InlineData("Custom.ContainsValue(1)")] + [InlineData("Custom.TryGetValue(\"k\", out _)")] public void Where_Dynamic_DictionaryMethods_ShouldThrowParseException_WhenMethodIsNotAccessible(string query) { // Arrange @@ -225,6 +249,23 @@ public void Where_Dynamic_DictionaryMethods_ShouldThrowParseException_WhenMethod items.Select(i => i.Custom["k"]).Should().OnlyContain(value => value == "v"); } + [Theory] + [InlineData("Meta.ContainsKey(\"k\")")] + [InlineData("Meta.ContainsValue(\"v\")")] + [InlineData("C.ContainsKey(\"k\")")] + public void Where_Dynamic_DictionaryMethods_AllowsStandardReadOperations(string query) + { + // Arrange + var item = new DictionaryValidationItem(); + item.C.TryAdd("k", "v"); + var items = new[] { item }.AsQueryable(); + + // Act & Assert + Assert.Same(item, Assert.Single(items.Where(query).ToList())); + Assert.Equal(new KeyValuePair("k", "v"), Assert.Single(item.Meta)); + Assert.Equal(new KeyValuePair("k", "v"), Assert.Single(item.C)); + } + [Fact] public void Where_Dynamic_StringQuoted() {