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 againstGET /api/identity/usersandGET /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:
- Secure by Default: Update the framework to utilize a static allow-list validation mechanism for
IGetListInput.Sortingby default, rejecting any unmapped or sensitive columns (likePasswordHash) with an HTTP 400 before they reach.OrderBy(). - Restrict ParsingConfig: Update the default ABP pipeline's
System.Linq.Dynamic.CoreParsingConfigto 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?
1 Answer(s)
-
0
Hi,
Confirmed — the
?sorting=query parameter on every list endpoint that consumesISortedResultRequestflows straight intoIQueryable.OrderBy(string)fromSystem.Linq.Dynamic.Core. The library'sRestrictOrderByToPropertyOrFielddefault in 1.6.0+ only blocks top-level keywords likeIIF(...)andSubstring(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 realORDER BY SUBSTRING(...)/CASE WHEN ... THEN 1 ELSE 0 ENDSQL, 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.QueryOptimizeris the public static hook the library exposes for inspecting expanded query trees; it fires on everyOrderBy(string)andThenBy(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 bootsAbpDddApplicationModuleworks — 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 descand any other plain property path (including nested ones likeTenant.Name) keep working?sorting=PasswordHash.Substring(0, 1) desc, ...,?sorting=PasswordHash.StartsWith("A") desc,?sorting=(PasswordHash > "X") descand 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
__tenantheader pivot and the missing per-IP rate limit on these endpoints: the rate-limit policy onAccount.SendPasswordResetCodedoes 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'sRateLimitermiddleware 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)