CVE-2026-69659 Overview
CVE-2026-69659 is an uncontrolled resource consumption vulnerability in the ash-project/ash framework for Elixir. The flaw resides in keyset pagination handling, where the decode_values/2 function in lib/ash/page/keyset.ex base64-decodes client-supplied page[:after] or page[:before] cursors and passes them to :erlang.binary_to_term/2 without size limits. Because the Erlang external term format transparently inflates zlib-compressed payloads, a cursor of a few kilobytes can allocate tens of megabytes of heap. Concurrent malicious requests can exhaust node memory and crash the Erlang runtime. The issue affects ash from version 1.17.0 before 3.31.1 and is classified as [CWE-502] Insecure Deserialization.
Critical Impact
A remote attacker can crash an Ash-backed Elixir node by submitting a small compressed pagination cursor that expands to tens of megabytes per request, aggregating into node-wide memory exhaustion.
Affected Products
- ash-project ash versions 1.17.0 through 3.31.0
- Elixir applications using Ash keyset pagination on public endpoints
- Any Ash consumer accepting client-supplied page[:after] or page[:before] cursors
Discovery Timeline
- 2026-08-09 - CVE-2026-69659 published to NVD
- 2026-08-12 - Last updated in NVD database
Technical Details for CVE-2026-69659
Vulnerability Analysis
Ash implements keyset pagination by serializing a record's sort values into an opaque cursor. The client passes this cursor back as page[:after] or page[:before] on subsequent requests. Inside decode_values/2 in lib/ash/page/keyset.ex, Ash base64-decodes the incoming cursor and calls :erlang.binary_to_term/2 directly on the result.
The Erlang external term format supports zlib-compressed payloads. binary_to_term/2 inflates these transparently, and Ash never bounded the resulting term size. Because Ash's own encoder never emits compressed cursors, the decoder accepts a shape the encoder cannot produce. Concurrent attacker requests compound the allocations and terminate the BEAM node.
Root Cause
The root cause is unsafe deserialization of untrusted binary input. :erlang.binary_to_term/2 was invoked without the size guardrails needed for external input, and no filter rejected compressed term payloads. This maps to [CWE-502] Deserialization of Untrusted Data.
Attack Vector
An unauthenticated attacker crafts a zlib-compressed Erlang external term, base64-encodes it, and submits it as the after or before pagination parameter to any Ash read action exposing keyset pagination. Each request forces the node to inflate the term, allocating tens of megabytes of heap per call. Sustained concurrent requests exhaust memory and crash the node.
# Security patch in lib/ash/page/keyset.ex - fix: limit keyset binary size
# Upper bound on the size (in bytes) of the term a client-supplied cursor is
# allowed to deserialize into. Cursors legitimately encode only the sort
# values of a single record, so this is generous; it exists purely to prevent
# a hostile cursor (e.g. a compressed term that inflates to tens of MB) from
# exhausting memory. Configurable via `config :ash, max_keyset_byte_size: ...`.
@default_max_keyset_byte_size 10_240
@derive {Inspect, only: [:results, :count, :before, :after, :more?]}
defstruct [:results, :count, :before, :after, :limit, :rerun, :more?]
Source: GitHub Commit Details
Detection Methods for CVE-2026-69659
Indicators of Compromise
- Elevated BEAM VM memory usage or repeated node crashes with system_limit or out-of-memory errors during pagination traffic
- Inbound HTTP requests carrying unusually long or high-entropy page[after] / page[before] query parameters
- Base64-decoded cursor payloads beginning with byte sequence 131, 80 (Erlang external term tag 131 followed by compressed term marker 80)
Detection Strategies
- Parse web access logs for pagination parameters exceeding the default 10 KiB threshold enforced by the patched max_keyset_byte_size
- Alert on Ash.Error.Page.InvalidKeyset exception spikes in application logs, which indicate rejected malformed or compressed cursors after patching
- Correlate memory pressure metrics on Elixir nodes with request rates targeting paginated read endpoints
Monitoring Recommendations
- Instrument BEAM telemetry to track per-process heap growth on request handlers serving Ash read actions
- Log the byte size of every decoded keyset cursor and alert on outliers
- Track HTTP request rate and payload size distributions per client IP against paginated endpoints
How to Mitigate CVE-2026-69659
Immediate Actions Required
- Upgrade ash to version 3.31.1 or later across all Elixir services using keyset pagination
- Audit any custom code that calls :erlang.binary_to_term/2 on client-supplied data and add [:safe] plus explicit size caps
- Rate-limit paginated read endpoints at the reverse proxy or web framework layer until the upgrade is deployed
Patch Information
The fix is delivered in ash 3.31.1 via commit 1816b103af975221210478d61db20adcea700319. The patch introduces @default_max_keyset_byte_size (10,240 bytes) as an upper bound on decoded cursor size, rejects compressed Erlang term payloads outright, and raises Ash.Error.Page.InvalidKeyset for malformed input. Applications sorting on unusually large values can raise the ceiling via configuration. See the GitHub Security Advisory and the CNA advisory for full details.
Workarounds
- Deploy a reverse-proxy rule that rejects requests where page[after] or page[before] exceeds 10 KiB base64-encoded
- Strip or block pagination parameters on public endpoints that do not require keyset pagination
- Add a request middleware that base64-decodes cursors and rejects payloads whose first bytes indicate a compressed Erlang term (<<131, 80, _::binary>>)
# Configuration example - raise the cap only if legitimate cursors exceed 10 KiB
# config/config.exs
config :ash, max_keyset_byte_size: 20_480
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

