Severity by source
CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Cursor is a network-accessible URL parameter requiring no authentication; SQL injection or in-process RCE constitutes a scope change beyond the vulnerable Ash component.
Primary rating from Vendor (6b3ad84c-e1a6-4bf7-a703-f496b71e49db).
CVSS VectorVendor: 6b3ad84c-e1a6-4bf7-a703-f496b71e49db
Lifecycle Timeline
4DescriptionCVE.org
Deserialization of Untrusted Data vulnerability in ash-project ash allows an unauthenticated attacker to inject a filter expression through a forged keyset pagination cursor, resulting in SQL injection or code execution depending on the data layer.
Read actions with keyset pagination decode the client-supplied page[:after] or page[:before] cursor in decode_values/2 in lib/ash/page/keyset.ex using non_executable_binary_to_term/2 with [:safe]. That guard blocks new atoms, funs, and ports, but not a struct built from atoms already interned in a running Ash application, so a decoded %Ash.Query.Call{} expression survives and is spliced into the keyset filter as a comparison value in do_filters/4 and evaluated. Because the cursor bypasses the Ash.Expr macro, the runtime never applies the private?/public? gate that would otherwise reject it. On AshPostgres the injected fragment is inlined into the SQL query; on the ETS and Simple data layers it is evaluated in-process as an arbitrary function call.
This issue affects ash: from 1.17.0 before 3.31.3.
AnalysisAI
Unauthenticated SQL injection and remote code execution in Ash Framework (ash-project/ash) are achievable by any caller who can reach a keyset-paginated endpoint: the cursor deserializer accepts pre-interned Ash expression structs that bypass query filter guards, yielding SQL injection on AshPostgres deployments or in-process arbitrary code execution on ETS and Simple data layers. All ash versions 1.17.0 through 3.31.2 are exposed across both impact paths. No independently released exploit tooling is known at time of analysis, but the upstream fix commit contains a working proof-of-concept that reproduces the exact forged cursor technique, substantially shortening the path from disclosure to weaponization.
Technical ContextAI
Ash Framework is an Elixir declarative resource and data layer abstraction used with AshPostgres, Ash.DataLayer.Ets, and Ash.DataLayer.Simple. The vulnerable code resides in decode_values/2 within lib/ash/page/keyset.ex, which decodes client-supplied keyset pagination cursors using Erlang's non_executable_binary_to_term/2 with the [:safe] option. The [:safe] flag prevents deserialization of new atoms, anonymous functions, and ports, but does not block reconstruction of structs whose atom keys are already present in the BEAM VM's atom table at runtime. Because Ash loads its own expression struct modules (including %Ash.Query.Call{}) at startup, those atoms are already interned, and a term_to_binary-serialized %Ash.Query.Call{} survives the [:safe] guard intact. The reconstructed struct is then passed to do_filters/4, which splices it into the keyset comparison filter as a value - a code path that never invokes the Ash.Expr macro's private?/public? access gate. On AshPostgres, the injected fragment is inlined directly into the Ecto SQL query (CWE-89 SQL Injection); on ETS and Simple layers, the expression is evaluated in-process as an arbitrary function call (CWE-502 Deserialization of Untrusted Data). The patch adds check_no_expression/1, which calls Ash.Expr.expr?/1 on the decoded term and returns {:error, %Ash.Error.Page.InvalidKeyset{}} for any expression-containing cursor.
RemediationAI
Upgrade to Ash version 3.31.3 or later, which resolves the issue by adding expression-type validation after deserialization in lib/ash/page/keyset.ex (commit 91874dd5435bc0ffebd8a254acfa573b39b74520; full advisory at https://github.com/ash-project/ash/security/advisories/GHSA-3gq3-9xm3-c8v3). If an immediate upgrade is not feasible, the most effective compensating control is to disable all read actions that expose keyset pagination - remove page type: :keyset configurations or add a plug/middleware that strips page[after] and page[before] parameters before they reach Ash - and replace them with offset-based pagination where possible; this eliminates the vulnerable decode_values/2 path entirely but changes client-facing pagination semantics and may break existing API consumers. A secondary control is to add an input validation plug that rejects any page[after] or page[before] value whose base64-decoded content begins with the Erlang external term format tag byte (<<131, ...>>), blocking binary term payloads at the HTTP layer; this is more brittle and should not substitute for patching, as encoding variations could bypass it.
Same weakness CWE-89 – SQL Injection
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-57544