Skip to content
Draft
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 33 additions & 1 deletion src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<string> AllowedDictionaryMethodNames =
[
"ContainsKey",
"ContainsValue",
"TryGetValue",
"get_Item"
];

private static readonly HashSet<MemberInfo> ObjectToStringAndObjectEquals =
[
Expand All @@ -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);
Expand Down
106 changes: 105 additions & 1 deletion test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -162,6 +163,109 @@ public void Where_Dynamic_Exceptions()
act.Should().Throw<ArgumentException>().WithMessage("The ParsingConfig should be provided as first argument to this method.*");
}

public class DictionaryValidationService
{
public bool Danger() => true;
}

public class CustomDictionary : Dictionary<string, string>, IDictionary<string, string>
{
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<string, string> Meta { get; set; } = new() { { "k", "v" } };

public ConcurrentDictionary<string, string> 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<DictionaryValidationItem>
{
new() { Name = "a" },
new() { Name = "b" }
}.AsQueryable();

// Act
Action act = () => items.Where(query).ToList();

// Assert
act.Should().Throw<ParseException>().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<string, string>("k", "v"), Assert.Single(item.Meta));
Assert.Equal(new KeyValuePair<string, string>("k", "v"), Assert.Single(item.C));
}

[Fact]
public void Where_Dynamic_StringQuoted()
{
Expand Down
Loading