CVE-2026-67579 Overview
CVE-2026-67579 is a deserialization of untrusted data vulnerability in the ash-project/ash Elixir framework. An unauthenticated attacker can forge a keyset pagination cursor to inject an %Ash.Query.Call{} filter expression. The injected expression is spliced into the keyset filter and evaluated at runtime. On AshPostgres the payload is inlined into SQL, producing SQL injection. On the ETS and Simple data layers the payload is evaluated in-process as an arbitrary function call, resulting in code execution. The vulnerability affects ash versions from 1.17.0 before 3.31.3 and is tracked under [CWE-89].
Critical Impact
Unauthenticated attackers can trigger SQL injection on AshPostgres or arbitrary code execution on ETS and Simple data layers by supplying a crafted page[:after] or page[:before] cursor.
Affected Products
- ash-project/ash versions >= 1.17.0 and < 3.31.3
- Applications using AshPostgres data layer (SQL injection impact)
- Applications using ETS or Simple data layers (arbitrary code execution impact)
Discovery Timeline
- 2026-08-12 - CVE-2026-67579 published to NVD
- 2026-08-12 - Last updated in NVD database
Technical Details for CVE-2026-67579
Vulnerability Analysis
Read actions using keyset pagination decode the client-supplied page[:after] or page[:before] cursor in decode_values/2 within lib/ash/page/keyset.ex. The decoder calls non_executable_binary_to_term/2 with the [:safe] option. That guard blocks new atoms, functions, and ports, but does not block structs constructed from atoms already interned in a running Ash application.
Because %Ash.Query.Call{} is a struct whose atoms are already loaded, a serialized instance survives decoding. The resulting term is then spliced into the keyset filter as a comparison value in do_filters/4 and evaluated. The cursor path bypasses the Ash.Expr macro, so the runtime never applies the private?/public? gate that would normally reject arbitrary expressions.
Root Cause
The root cause is trusting the output of non_executable_binary_to_term/2 with [:safe] as sufficient validation for user-supplied binary terms. The :safe flag does not prevent reconstruction of struct types whose atoms are already interned. No expression-type check was performed on the decoded term before it entered the filter evaluation pipeline.
Attack Vector
An unauthenticated attacker constructs a Base64-encoded Erlang term containing an %Ash.Query.Call{} struct and submits it as a page[:after] or page[:before] cursor to any read action supporting keyset pagination. On AshPostgres, the injected fragment becomes part of the emitted SQL. On ETS and Simple data layers, the call is evaluated as an in-process function invocation.
// Security patch in lib/ash/page/keyset.ex
// Source: https://github.com/ash-project/ash/commit/91874dd5435bc0ffebd8a254acfa573b39b74520
Application.get_env(:ash, :max_keyset_byte_size, @default_max_keyset_byte_size)
with {:ok, decoded} <- Base.decode64(values),
- :ok <- check_keyset_size(decoded, max_byte_size) do
- {:ok, non_executable_binary_to_term(decoded, [:safe])}
+ :ok <- check_keyset_size(decoded, max_byte_size),
+ term <- non_executable_binary_to_term(decoded, [:safe]),
+ :ok <- check_no_expression(term) do
+ {:ok, term}
else
_ ->
{:error, Ash.Error.Page.InvalidKeyset.exception(value: values, key: key)}
The patch adds a check_no_expression/1 guard that rejects any decoded term containing an Ash.Expr construct before it is returned to the caller.
Detection Methods for CVE-2026-67579
Indicators of Compromise
- Unusually long or structurally complex page[:after] or page[:before] query parameters submitted to Ash read actions.
- Base64-decoded cursor payloads that contain the byte pattern for the atom Ash.Query.Call or other Ash.Expr struct names.
- Unexpected SQL fragments appearing in AshPostgres query logs, particularly within WHERE clauses generated from keyset comparisons.
- Elixir process crashes or unexpected function invocations correlated with paginated read requests on ETS or Simple data layer resources.
Detection Strategies
- Inspect Phoenix or Plug access logs for keyset cursor parameters exceeding typical opaque token size or containing decoded Erlang External Term Format markers.
- Enable SQL statement logging on AshPostgres and alert on WHERE clauses that reference unexpected function calls or fragments not present in the application schema.
- Add runtime instrumentation to Ash.Page.Keyset.decode_values/2 to log decoded term types during triage windows.
Monitoring Recommendations
- Forward web application and database query logs to a centralized analytics platform and baseline the normal distribution of keyset cursor lengths.
- Alert on any deviation from the baseline cursor length or on the presence of Erlang term markers such as 131, 116 after Base64 decoding.
- Monitor Elixir application telemetry for anomalous function calls originating from pagination request handlers.
How to Mitigate CVE-2026-67579
Immediate Actions Required
- Upgrade ash to version 3.31.3 or later in mix.exs and redeploy affected applications.
- Audit application logs for suspicious page[:after] and page[:before] values submitted before the upgrade.
- Review AshPostgres query logs for evidence of SQL injection via keyset filters.
- Rotate database credentials if evidence of SQL injection is confirmed.
Patch Information
The fix is contained in commit 91874dd5435bc0ffebd8a254acfa573b39b74520 and published in ash 3.31.3. See the GitHub Security Advisory GHSA-3gq3-9xm3-c8v3 and the CNA Security Advisory for full details. The patch introduces a check_no_expression/1 guard that inspects decoded terms and rejects any that contain Ash.Expr structs.
Workarounds
- Disable keyset pagination on public, unauthenticated read actions until the upgrade to 3.31.3 is deployed.
- Restrict the :max_keyset_byte_size application environment value to the smallest size that fits legitimate cursors, reducing payload capacity for injected expressions.
- Place a reverse proxy filter in front of the application that rejects page[:after] and page[:before] parameters exceeding an expected byte length or matching Erlang External Term Format signatures.
# Update the ash dependency in mix.exs and fetch the patched release
# {:ash, "~> 3.31.3"}
mix deps.update ash
mix deps.get
mix compile
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

