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 add680a5..175e8f4d 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2323,6 +2323,13 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string if (type != null && TypeHelper.IsDictionary(type) && _methodFinder.ContainsMethod(type, methodName, false)) { + 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(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 a3b6d85e..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,6 +31,27 @@ internal static class PredefinedMethodsHelper ObjectStaticReferenceEquals ]; + public static bool IsPredefinedDictionaryMethod(MethodInfo method) + { + Check.NotNull(method); + + 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) { Check.NotNull(config); diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index f5300898..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; @@ -162,6 +163,109 @@ 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 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 + { + 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 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\")")] + [InlineData("Custom.ContainsKey(\"k\")")] + [InlineData("Custom.ContainsValue(1)")] + [InlineData("Custom.TryGetValue(\"k\", out _)")] + 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().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"); + } + + [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() {