Skip to content
Merged
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: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ If it's not possible to add that attribute, you need to implement a custom [Cust
Or provide a list of addtional types in the [DefaultDynamicLinqCustomTypeProvider.cs](https://github.com/zzzprojects/System.Linq.Dynamic.Core/blob/master/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs).

### v1.6.0-preview-01, 02, 03
A breaking change is introduced in this version to solve CVE-2024-51417.
#### Change 1
It's not allowed anymore to call any methods on the `object` type. By default also the `ToString` and `Equals` methods are not allowed.
To allow these methods set `AllowEqualsAndToStringMethodsOnObject` to `true` in the `ParsingConfig` and provide that config to all dynamic calls.
This is done to mitigate the risk of calling methods on the `object` type which could lead to security issues (CVE-2024-51417).

#### Change 2
By default the `RestrictOrderByToPropertyOrField` is now set to `true` in the `ParsingConfig`.
Which means that only properties and fields can be used in the `OrderBy` / `ThenBy`.
This is done to mitigate the risk of calling methods or other expressions in the `OrderBy` / `ThenBy` which could lead to security issues.

---

Expand Down
4 changes: 3 additions & 1 deletion src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,9 @@

if (isValid && shouldPrioritizeType)
{
var keywordOrFunctionAllowed = !_usedForOrderBy || _usedForOrderBy && !_parsingConfig.RestrictOrderByToPropertyOrField;
var keywordOrFunctionAllowed =
!_usedForOrderBy ||
(_usedForOrderBy && (_keywordsHelper.IsItOrRootOrParent(keywordOrType) || !_parsingConfig.RestrictOrderByToPropertyOrField));
if (!keywordOrFunctionAllowed)
{
throw ParseError(Res.UnknownPropertyOrField, _textParser.CurrentToken.Text, TypeHelper.GetTypeName(_it?.Type));
Expand Down Expand Up @@ -1932,8 +1934,8 @@
switch (member)
{
case PropertyInfo property:
var propertyIsStatic = property?.GetGetMethod().IsStatic ?? property?.GetSetMethod().IsStatic ?? false;

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.

Check warning on line 1937 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Dereference of a possibly null reference.
propertyOrFieldExpression = propertyIsStatic ? Expression.Property(null, property) : Expression.Property(expression, property);

Check warning on line 1938 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Possible null reference argument for parameter 'property' in 'MemberExpression Expression.Property(Expression? expression, PropertyInfo property)'.

Check warning on line 1938 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Possible null reference argument for parameter 'property' in 'MemberExpression Expression.Property(Expression? expression, PropertyInfo property)'.

Check warning on line 1938 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Possible null reference argument for parameter 'property' in 'MemberExpression Expression.Property(Expression? expression, PropertyInfo property)'.

Check warning on line 1938 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Linux: Build and Tests

Possible null reference argument for parameter 'property' in 'MemberExpression Expression.Property(Expression? expression, PropertyInfo property)'.
return true;

case FieldInfo field:
Expand Down
2 changes: 2 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/IKeywordsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ namespace System.Linq.Dynamic.Core.Parser;

internal interface IKeywordsHelper
{
bool IsItOrRootOrParent(AnyOf<string, Expression, Type> value);

bool TryGetValue(string text, out AnyOf<string, Expression, Type> value);
}
10 changes: 10 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ public KeywordsHelper(ParsingConfig config)
}
}

public bool IsItOrRootOrParent(AnyOf<string, Expression, Type> value)
{
if (value.IsFirst)
{
return value.First is KEYWORD_IT or KEYWORD_ROOT or KEYWORD_PARENT or SYMBOL_IT or SYMBOL_PARENT or SYMBOL_ROOT;
}

return false;
}

public bool TryGetValue(string text, out AnyOf<string, Expression, Type> value)
{
// 1. Try to get as constant-expression, keyword, symbol or functions
Expand Down
6 changes: 3 additions & 3 deletions src/System.Linq.Dynamic.Core/ParsingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,11 @@ public IQueryableAnalyzer QueryableAnalyzer
public StringLiteralParsingType StringLiteralParsing { get; set; } = StringLiteralParsingType.Default;

/// <summary>
/// When set to <c>true</c>, the parser will restrict the OrderBy method to only allow properties or fields.
/// When set to <c>true</c>, the parser will restrict the OrderBy and ThenBy methods to only allow properties or fields. If set to <c>false</c>, any expression is allowed.
///
/// Default value is <c>false</c>.
/// Default value is <c>true</c>.
/// </summary>
public bool RestrictOrderByToPropertyOrField { get; set; }
public bool RestrictOrderByToPropertyOrField { get; set; } = true;

/// <summary>
/// When set to <c>true</c>, the parser will allow the use of the Equals(object obj), Equals(object objA, object objB), ReferenceEquals(object objA, object objB) and ToString() methods on the <see cref="object"/> type.
Expand Down
45 changes: 24 additions & 21 deletions test/System.Linq.Dynamic.Core.Tests/EntitiesTests.OrderBy.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Dynamic.Core.Tests.Helpers.Entities;
using FluentAssertions;
using Xunit;
Expand All @@ -7,57 +8,58 @@ namespace System.Linq.Dynamic.Core.Tests;

public partial class EntitiesTests
{
[Fact]
public void Entities_OrderBy_RestrictOrderByIsFalse()
[Theory]
[InlineData("IIF(1 == 1, 1, 0)")]
[InlineData("np(Name, \"x\")")]
public void Entities_OrderBy_RestrictOrderByIsFalse_ShouldAllowAnyExpression(string expression)
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };

// Act
var resultBlogs = _context.Blogs.OrderBy(b => true).ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy("IIF(1 == 1, 1, 0)").ToDynamicArray<Blog>();
Action action = () => _ = _context.Blogs.OrderBy(config, expression).ToDynamicArray<Blog>();

// Assert
Assert.Equal(resultBlogs, dynamicResultBlogs);
action.Should().NotThrow();
}

[Fact]
public void Entities_OrderBy_RestrictOrderByIsTrue_ValidExpressionShouldNotThrow()
public void Entities_OrderBy_RestrictOrderByIsTrue_NonRestrictedExpressionShouldNotThrow()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = true };

// Act 1
var resultBlogs = _context.Blogs.OrderBy(b => b.Name).ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy(config, "Name").ToDynamicArray<Blog>();
var dynamicResultBlogs = _context.Blogs.OrderBy("Name").ToDynamicArray<Blog>();

// Assert 1
Assert.Equal(resultBlogs, dynamicResultBlogs);

// Act 2
var resultPosts = _context.Posts.OrderBy(p => p.Blog.Name).ToArray();
var dynamicResultPosts = _context.Posts.OrderBy(config, "Blog.Name").ToDynamicArray<Post>();
var dynamicResultPosts = _context.Posts.OrderBy("Blog.Name").ToDynamicArray<Post>();

// Assert 2
Assert.Equal(resultPosts, dynamicResultPosts);
}

[Fact]
public void Entities_OrderBy_RestrictOrderByIsTrue_InvalidExpressionShouldThrow()

[Theory]
[InlineData("IIF(1 == 1, 1, 0)")]
[InlineData("np(Name, \"x\")")]
public void Entities_OrderBy_RestrictOrderByIsTrue_RestrictedExpressionShouldThrow(string expression)
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = true };

// Act
Action action = () => _context.Blogs.OrderBy(config, "IIF(1 == 1, 1, 0)");
Action action = () => _context.Blogs.OrderBy(expression);

// Assert
action.Should().Throw<ParseException>().WithMessage("No property or field 'IIF' exists in type 'Blog'");
action.Should().Throw<ParseException>().WithMessage("No property or field '*' exists in type 'Blog'");
}

[Fact]
public void Entities_OrderBy_NullPropagation_Int()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var resultBlogs = _context.Blogs.OrderBy(b => b.NullableInt ?? -1).ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy("np(NullableInt, -1)").ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy(config, "np(NullableInt, -1)").ToArray();

// Assert
Assert.Equal(resultBlogs, dynamicResultBlogs);
Expand All @@ -67,8 +69,9 @@ public void Entities_OrderBy_NullPropagation_Int()
public void Entities_OrderBy_NullPropagation_String()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var resultBlogs = _context.Blogs.OrderBy(b => b.X ?? "_").ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy("np(X, \"_\")").ToArray();
var dynamicResultBlogs = _context.Blogs.OrderBy(config, "np(X, \"_\")").ToArray();

// Assert
Assert.Equal(resultBlogs, dynamicResultBlogs);
Expand Down
12 changes: 9 additions & 3 deletions test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,8 @@ public void ExpressionTests_IsNull_ThrowsException()
[Fact]
public void ExpressionTests_Indexer_Issue57()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = true };
var rows = new List<JObject>
{
new JObject {["Column1"] = "B", ["Column2"] = 1},
Expand All @@ -1392,9 +1394,11 @@ public void ExpressionTests_Indexer_Issue57()
new JObject {["Column1"] = "A", ["Column2"] = 2}
};

// Act
var expected = rows.OrderBy(x => x["Column1"]).ToList();
var result = rows.AsQueryable().OrderBy(@"it[""Column1""]").ToList();
var result = rows.AsQueryable().OrderBy(config, @"it[""Column1""]").ToList();

// Assert
Assert.Equal(expected, result);
}

Expand Down Expand Up @@ -1727,14 +1731,15 @@ public void ExpressionTests_NullPropagation_Method_WithDefaultValue()
public void ExpressionTests_NullPropagating_DateTime()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var q = new[]
{
new { id = 1, date1 = (DateTime?) DateTime.Now, date2 = DateTime.Now.AddDays(-1) }
}.AsQueryable();

// Act
var result = q.OrderBy(x => x.date2).Select(x => x.id).ToArray();
var resultDynamic = q.OrderBy("np(date1)").Select("id").ToDynamicArray<int>();
var resultDynamic = q.OrderBy(config, "np(date1)").Select("id").ToDynamicArray<int>();

// Assert
Check.That(resultDynamic).ContainsExactly(result);
Expand All @@ -1744,14 +1749,15 @@ public void ExpressionTests_NullPropagating_DateTime()
public void ExpressionTests_NullPropagation_NullableDateTime()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var q = new[]
{
new { id = 1, date1 = (DateTime?) DateTime.Now, date2 = DateTime.Now.AddDays(-1)}
}.AsQueryable();

// Act
var result = q.OrderBy(x => x.date1.Value.Year).Select(x => x.id).ToArray();
var resultDynamic = q.OrderBy("np(date1.Value.Year)").Select("id").ToDynamicArray<int>();
var resultDynamic = q.OrderBy(config, "np(date1.Value.Year)").Select("id").ToDynamicArray<int>();

// Assert
Check.That(resultDynamic).ContainsExactly(result);
Expand Down
34 changes: 30 additions & 4 deletions test/System.Linq.Dynamic.Core.Tests/QueryableTests.OrderBy.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Parser;
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
using FluentAssertions;
using Xunit;

namespace System.Linq.Dynamic.Core.Tests;
Expand Down Expand Up @@ -36,12 +38,13 @@ public int Compare(object? x, object? y)
public void OrderBy_Dynamic_NullPropagation_Int()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var testList = User.GenerateSampleModels(2);
var qry = testList.AsQueryable();

// Act
var orderBy = testList.OrderBy(x => x.NullableInt ?? -1).ToArray();
var orderByDynamic = qry.OrderBy("np(NullableInt, -1)").ToArray();
var orderByDynamic = qry.OrderBy(config, "np(NullableInt, -1)").ToArray();

// Assert
Assert.Equal(orderBy, orderByDynamic);
Expand All @@ -51,12 +54,13 @@ public void OrderBy_Dynamic_NullPropagation_Int()
public void OrderBy_Dynamic_NullPropagation_String()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var testList = User.GenerateSampleModels(2);
var qry = testList.AsQueryable();

// Act
var orderBy = testList.OrderBy(x => x.NullableString ?? "_").ToArray();
var orderByDynamic = qry.OrderBy("np(NullableString, \"_\")").ToArray();
var orderByDynamic = qry.OrderBy(config, "np(NullableString, \"_\")").ToArray();

// Assert
Assert.Equal(orderBy, orderByDynamic);
Expand All @@ -66,12 +70,13 @@ public void OrderBy_Dynamic_NullPropagation_String()
public void OrderBy_Dynamic_NullPropagation_NestedObject()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var testList = User.GenerateSampleModels(2);
var qry = testList.AsQueryable();

// Act
var orderBy = testList.OrderBy(x => x.Profile?.Age ?? -1).ToArray();
var orderByDynamic = qry.OrderBy("np(Profile.Age, -1)").ToArray();
var orderByDynamic = qry.OrderBy(config, "np(Profile.Age, -1)").ToArray();

// Assert
Assert.Equal(orderBy, orderByDynamic);
Expand All @@ -81,6 +86,7 @@ public void OrderBy_Dynamic_NullPropagation_NestedObject()
public void OrderBy_Dynamic_NullPropagation_NestedObject_Query()
{
// Arrange
var config = new ParsingConfig { RestrictOrderByToPropertyOrField = false };
var qry = User.GenerateSampleModels(2)
.Select(u => new
{
Expand All @@ -93,7 +99,7 @@ public void OrderBy_Dynamic_NullPropagation_NestedObject_Query()
.AsQueryable();

// Act
var orderByDynamic = qry.OrderBy("np(X.Age, -1)").ToArray();
var orderByDynamic = qry.OrderBy(config, "np(X.Age, -1)").ToArray();

// Assert
Assert.NotNull(orderByDynamic);
Expand Down Expand Up @@ -238,4 +244,24 @@ public void OrderBy_Dynamic_Exceptions()
Assert.Throws<ArgumentException>(() => qry.OrderBy(""));
Assert.Throws<ArgumentException>(() => qry.OrderBy(" "));
}

[Theory]
[InlineData(KeywordsHelper.KEYWORD_IT)]
[InlineData(KeywordsHelper.SYMBOL_IT)]
[InlineData(KeywordsHelper.KEYWORD_ROOT)]
[InlineData(KeywordsHelper.SYMBOL_ROOT)]
[InlineData("\"User\" + \"Name\"")]
[InlineData("\"User\" + \"Name\" asc")]
[InlineData("\"User\" + \"Name\" desc")]
public void OrderBy_RestrictOrderByIsTrue_NonRestrictedExpressionShouldNotThrow(string expression)
{
// Arrange
var queryable = User.GenerateSampleModels(3).AsQueryable();

// Act
Action action = () => _ = queryable.OrderBy(expression);

// Assert 2
action.Should().NotThrow();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Linq.Dynamic.Core.Exceptions;
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
using Xunit;

namespace System.Linq.Dynamic.Core.Tests
Expand Down
Loading