Open Closed

Security Finding in ABP v9.2.4: Dynamic LINQ Expression Injection via Paged List Endpoints (?sorting=) #10728


User avatar
0
murat.kebabci created

Our recent penetration test flagged a high-severity vulnerability (CWE-94, CVE-2023-32571 class) regarding how the ABP framework natively handles the sorting parameter in its built-in paged list endpoints.

Affected Endpoints:

  • Virtually every default paged ABP list endpoint exposing ?sorting= (Verified specifically against GET /api/identity/users and GET /api/audit-logging/audit-logs, affecting ~80 built-in endpoints).

Issue Details: The ?sorting= query string is natively passed to System.Linq.Dynamic.Core by the framework without a strict allow-list mechanism. Consequently, arbitrary entity properties—including critical server-only columns like PasswordHash, SecurityStamp, and ConcurrencyStamp—along with string method invocations (e.g., .Substring, .Length) and ternary boolean predicates, are evaluated server-side. This creates a data-exfiltration blind oracle out-of-the-box. Server-only columns are accepted as sort keys, and rows are ordered by their actual values, allowing attackers to extract sensitive data character by character.

Requested Framework-Level Remediation: Since this vulnerability stems from the core framework's default implementation, we are requesting a structural patch/fix in the ABP framework itself rather than implementing manual overrides for every endpoint. Specifically, we request that the ABP team:

  1. Secure by Default: Update the framework to utilize a static allow-list validation mechanism for IGetListInput.Sorting by default, rejecting any unmapped or sensitive columns (like PasswordHash) with an HTTP 400 before they reach .OrderBy().
  2. Restrict ParsingConfig: Update the default ABP pipeline's System.Linq.Dynamic.Core ParsingConfig to strictly disable method-invocation, ternary-conditional, and substring-style accessors.

Could you please confirm if this issue is already being tracked, and in which upcoming release we can expect a framework-level patch for this vulnerability?

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

1 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Confirmed — the ?sorting= query parameter on every list endpoint that consumes ISortedResultRequest flows straight into IQueryable.OrderBy(string) from System.Linq.Dynamic.Core. The library's RestrictOrderByToPropertyOrField default in 1.6.0+ only blocks top-level keywords like IIF(...) and Substring(x, ...); it does not block method calls on a property (PasswordHash.Substring(0, 1)), comparison expressions (PasswordHash > "X") or chained predicates. EF Core translates those into real ORDER BY SUBSTRING(...) / CASE WHEN ... THEN 1 ELSE 0 END SQL, so an authenticated user with list access can byte-extract sensitive columns through the row ordering, even when those columns are never in the response DTO.

    The fix has to sit outside Dynamic.Core. ExtensibilityPoint.QueryOptimizer is the public static hook the library exposes for inspecting expanded query trees; it fires on every OrderBy(string) and ThenBy(string) call regardless of which layer the caller sits in (MVC, Blazor, background job, direct DI call), so a single registration covers all of your list endpoints at once.

    Drop this class into your *.HttpApi.Host (any host that boots AbpDddApplicationModule works — adjust the namespace to fit your project):

    using System;
    using System.Linq;
    using System.Linq.Dynamic.Core;
    using System.Linq.Expressions;
    using Volo.Abp.Validation;
    
    namespace MyCompanyName.MyProjectName;
    
    public static class DynamicSortingGuard
    {
        private static readonly object InstallLock = new();
        private static Func<Expression, Expression> _activeOptimizer;
    
        public static void Install()
        {
            lock (InstallLock)
            {
                var current = ExtensibilityPoint.QueryOptimizer;
                if (_activeOptimizer != null && ReferenceEquals(current, _activeOptimizer))
                {
                    return;
                }
    
                var previous = current;
                _activeOptimizer = expression =>
                {
                    new OrderByMethodVisitor().Visit(expression);
                    return previous != null ? previous(expression) : expression;
                };
                ExtensibilityPoint.QueryOptimizer = _activeOptimizer;
            }
        }
    
        private sealed class OrderByMethodVisitor : ExpressionVisitor
        {
            protected override Expression VisitMethodCall(MethodCallExpression node)
            {
                if (node.Method.DeclaringType == typeof(Queryable) &&
                    IsOrderByMethod(node.Method.Name) &&
                    node.Arguments.Count >= 2 &&
                    node.Arguments[1] is UnaryExpression { Operand: LambdaExpression lambda })
                {
                    new PropertyOnlySelectorVisitor().Visit(lambda.Body);
                }
    
                return base.VisitMethodCall(node);
            }
    
            private static bool IsOrderByMethod(string name)
            {
                return name == nameof(Queryable.OrderBy)
                    || name == nameof(Queryable.OrderByDescending)
                    || name == nameof(Queryable.ThenBy)
                    || name == nameof(Queryable.ThenByDescending);
            }
        }
    
        private sealed class PropertyOnlySelectorVisitor : ExpressionVisitor
        {
            private const string Message = "Sorting expression is not supported.";
    
            protected override Expression VisitMethodCall(MethodCallExpression node)
                => throw new AbpValidationException(Message);
    
            protected override Expression VisitBinary(BinaryExpression node)
                => throw new AbpValidationException(Message);
    
            protected override Expression VisitConditional(ConditionalExpression node)
                => throw new AbpValidationException(Message);
    
            protected override Expression VisitConstant(ConstantExpression node)
                => throw new AbpValidationException(Message);
        }
    }
    

    Then call it once from your host module:

    public override void PreConfigureServices(ServiceConfigurationContext context)
    {
        DynamicSortingGuard.Install();
    }
    

    That's the whole change. After deploying:

    • ?sorting=Name desc and any other plain property path (including nested ones like Tenant.Name) keep working
    • ?sorting=PasswordHash.Substring(0, 1) desc, ..., ?sorting=PasswordHash.StartsWith("A") desc, ?sorting=(PasswordHash > "X") desc and similar expression payloads now return HTTP 400 with a generic "Your request is not valid!" body — the specific reason is logged on the server, not echoed to the client, so an attacker can't probe what shape is allowed

    A built-in version of this guard is going into the framework so future releases ship the same protection out of the box without any host-side code. Plain property sorting by sensitive columns (e.g. ?sorting=PasswordHash) is a separate hardening layer — the right place to address that is a DTO-property allow-list, and we're tracking it as a follow-up to this fix.

    About the __tenant header pivot and the missing per-IP rate limit on these endpoints: the rate-limit policy on Account.SendPasswordResetCode does exist but partitions by email, so it doesn't apply here. We don't ship a built-in per-IP throttle for list endpoints because the right threshold depends on your deployment — ASP.NET Core's RateLimiter middleware scoped to /api/identity/* (or a rule at your reverse proxy / ingress) is the standard place for that.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.