Skip to main content
Vulnerability Database/CVE-2026-81638

CVE-2026-81638: ash_double_entry Auth Bypass Vulnerability

CVE-2026-81638 is an authentication bypass flaw in ash_double_entry allowing attackers to exploit alternate ULID encodings to bypass security checks. This post explains its impact, affected versions, and mitigation steps.

Published:

CVE-2026-81638 Overview

CVE-2026-81638 is an Improper Handling of Alternate Encoding vulnerability [CWE-173] in the ash-project/ash_double_entry library. The AshDoubleEntry.ULID type renders a 128-bit ULID as 26 Crockford base-32 characters, but the first character encodes only 3 bits. The decode/1 function in lib/ulid.ex masks the first character to its low 3 bits, and valid?/1 accepts all 32 characters in that position. As a result, ULIDs starting with 0, 8, G, or R decode to the identical 16-byte value and resolve to the same database row. Affected versions include ash_double_entry from 0.1.0 before 1.0.19.

Critical Impact

Attackers can submit multiple distinct string spellings of the same identifier, desynchronizing idempotency keys, deduplication logic, deny-lists, audit correlation, and signatures computed over the submitted ID.

Affected Products

  • ash-project/ash_double_entry versions 0.1.0 through 1.0.18
  • Elixir/Ash applications exposing AshDoubleEntry.ULID as a public ID over HTTP or API boundaries
  • Any downstream system relying on string equality of these IDs for security decisions

Discovery Timeline

  • 2026-09-07 - CVE-2026-81638 published to NVD
  • 2026-09-08 - Last updated in NVD database

Technical Details for CVE-2026-81638

Vulnerability Analysis

The AshDoubleEntry.ULID type encodes a 128-bit ULID into 26 Crockford base-32 characters. Each base-32 character normally encodes 5 bits, yielding 130 bits total. To fit 128 bits, the encoder writes only the low 3 bits of the first character. The decoder in lib/ulid.ex mirrors this by masking the first character to 3 bits during decoding, but the valid?/1 check accepts any of the 32 Crockford characters in that leading position. This asymmetry means four different characters (0, 8, G, R) map to bit pattern 000 after masking, and similarly for values 1/9/H/S, and so on. Each canonical ULID therefore has three non-canonical alternate spellings that decode to the same 16-byte binary and resolve to the same record.

Root Cause

The root cause is a violation of the invariant encode(decode(x)) == x. Non-canonical inputs are accepted by validation but silently normalized during decoding. This is a textbook alternate encoding flaw [CWE-173], where security-relevant logic operating on the string form of an identifier sees a different value than the storage layer operating on the decoded form.

Attack Vector

An attacker submits an ID with a modified leading character over an HTTP or API boundary. String-level checks such as idempotency keys, deduplication tables, deny-lists, HMAC signatures over the ID string, or audit-log correlation compare the raw submitted string. The database layer decodes the ID and returns the same row as the canonical spelling. This allows replay of previously-processed operations, bypass of deny-listed IDs, forged audit trails, or signature reuse across alternate spellings of the same resource.

text
            c13::8, c14::8, c15::8, c16::8, c17::8, c18::8, c19::8, c20::8, c21::8, c22::8, c23::8,
            c24::8, c25::8, c26::8>>
        ) do
+    # The first Crockford character encodes only 3 bits (encode/1 writes
+    # `b1::3`). Reject any input whose first character sets the unused high
+    # bits (decodes above 7) so that each ULID has exactly one canonical
+    # spelling and `encode(decode(x)) == x` for every accepted value.
+    if d(c1) > 7, do: throw(:error)
+
     <<d(c1)::3, d(c2)::5, d(c3)::5, d(c4)::5, d(c5)::5, d(c6)::5, d(c7)::5, d(c8)::5, d(c9)::5,
       d(c10)::5, d(c11)::5, d(c12)::5, d(c13)::5, d(c14)::5, d(c15)::5, d(c16)::5, d(c17)::5,
       d(c18)::5, d(c19)::5, d(c20)::5, d(c21)::5, d(c22)::5, d(c23)::5, d(c24)::5, d(c25)::5,

Source: GitHub Commit d3e688d. The patch rejects any input whose first character decodes above 7, enforcing a single canonical spelling per ULID.

Detect Identity Attacks the Moment Authentication Fails

Singularity™ Identity surfaces credential misuse, lateral movement, and privilege escalation in real time, so you can contain the breach before it spreads.

Stop Identity Attacks

Detection Methods for CVE-2026-81638

Indicators of Compromise

  • Requests containing ULID path or body parameters whose first character is 8, 9, A-H, J-N, P-R, S-Z (any character where d(c1) > 7).
  • Multiple audit-log entries referencing distinct ULID strings that resolve to the same database record.
  • Idempotency-key hits followed by successful reprocessing of an apparently new request with a differently-spelled ID.

Detection Strategies

  • Log the raw submitted ID string alongside the canonical re-encoded form and alert when they differ.
  • Compare application-layer ID strings against database primary-key strings to surface encoding drift.
  • Run static analysis on Elixir dependencies to flag ash_double_entry versions below 1.0.19.

Monitoring Recommendations

  • Instrument HTTP handlers that accept ULIDs to record request rate by first-character class and alert on non-0-7 prefixes.
  • Monitor idempotency and deduplication stores for entries whose decoded binary matches an existing key but whose string form differs.
  • Correlate signature verification failures with subsequent successful retries using the same underlying resource.

How to Mitigate CVE-2026-81638

Immediate Actions Required

  • Upgrade ash_double_entry to version 1.0.19 or later across all Elixir applications.
  • Audit existing idempotency, deduplication, and deny-list stores for entries with non-canonical first characters and normalize them.
  • Review audit logs for operations referencing ULIDs whose first character exceeds 7 and validate that no unauthorized reprocessing occurred.

Patch Information

The fix is committed in GitHub Commit d3e688d and released in ash_double_entry1.0.19. Full details are in the GitHub Security Advisory GHSA-qxp2-vgp9-268q, the CNA CVE-2026-81638 Details, and the OSV Vulnerability EEF-CVE-2026-81638 record. The patch adds a guard in decode/1 that rejects inputs whose first character decodes above 7.

Workarounds

  • If patching is not immediately possible, add an application-layer validator that rejects ULIDs whose first character is not in the set 0-7.
  • Canonicalize all inbound ULIDs by decoding and re-encoding before using them in idempotency keys, signatures, or deny-list lookups.
  • Enforce a database-level constraint or check that stored ULID primary keys always match their canonical string form.
bash
# Update the dependency in mix.exs and fetch the patched release
# mix.exs
# {:ash_double_entry, "~> 1.0.19"}
mix deps.update ash_double_entry
mix deps.get
mix compile --force

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.