ash_typescript 0.18.0 fixes seven CVEs across RPC and generated clients
Two BEAM atom-table exhaustion paths, denied-field disclosure, missing constraints and URL bugs expose risks at the Elixir–TypeScript boundary.
- AUTHOR
- Karol Rapacz / CEO Breachroad · OSCP · PNPT
- PUBLISHED
- 1 September 2026
- READING TIME
- 21 min read
- TOPIC
- Vulnerabilities and CVEs
Seven vulnerabilities in ash_typescript, a library generating a typed interface between Ash/Elixir applications and TypeScript clients, entered today’s NVD feeds. The most severe issues let an unauthenticated user crash the whole BEAM node by permanently creating atoms or read a value denied by an Ash field policy. The remaining flaws concern raw error disclosure, skipped argument constraints and unencoded path parameters.
All seven are fixed in ash_typescript 0.18.0. Starting versions vary by feature: the oldest issue dates to 0.1.0, while others begin in 0.8.0, 0.11.0 or 0.15.0. Version 0.18.0 arrived on the evening of 31 August and the CVE records were published in NVD on 1 September. The set demonstrates that a generated TypeScript type helps developers but is not a server-side security control.
CVE-2026-74837 and CVE-2026-77856: permanent atoms from client field names
The BEAM virtual machine stores atoms in a global table. They work well as identifiers known while building a program, but they are not garbage-collected. Each new value remains for the node’s lifetime. Once the table limit is reached, the VM terminates. For that reason, String.to_atom/1 on arbitrary network input is a recognised security anti-pattern.
In CVE-2026-74837, an RPC client supplied selected field names. parse_input_field/2 tried String.to_existing_atom/1, but returned a string for an unknown value. The next step, convert_to_field_atom/2, turned it into an atom with String.to_atom/1 before confirming that the field existed. Every distinct name permanently grew the table. A name over 255 characters could also raise an uncaught SystemLimitError.
CVE-2026-77856 is a parallel path for typed-struct fields. When the reverse map lacked a name, resolve_typed_struct_field/2 likewise used String.to_atom/1. An attacker did not need schema knowledge: failure to match was precisely what minted a value.
The first flaw scores 8.7 under CVSS 4.0 and affects versions from 0.1.0. The second scores 8.2 and starts at 0.11.0. The impact exceeds a failed request. The atom table belongs to the node, and its consumption is not reclaimed without restart. Low-rate traffic spread over time can accumulate permanent cost.
CVE-2026-82730: ForbiddenField disclosed original_value
Ash field policies can deny an attribute read. The framework substitutes %Ash.ForbiddenField{}. This marker retains the real value in original_value because embedded resources need to remain writable, but hides it from normal Inspect output. ash_typescript converted such markers to nil in template-driven result paths.
Not every serialisation used that route. normalize_primitive/1 had no dedicated ForbiddenField clause, so the marker reached generic struct handling. Map.from_struct/1 converted it into an ordinary map containing every key, including original_value. An RPC response could consequently contain both the denial marker and the secret value that had been denied.
The simplest route is an action returning an embedded resource as a map and processing it without a template. Public normalize_value_for_json/1 reached the same behaviour. CVE-2026-82730 scores 8.2 and affects releases from 0.11.0. Redaction in Inspect may protect a console or log, but it does not automatically protect every serializer.
CVE-2026-82732: a value was cast but its constraints were not applied
The typed controller accepted an HTTP argument and called Ash.Type.cast_input/3. It treated {:ok, cast} as complete validation. Ash separates casting from constraints. cast_input converts the representation to a type, while Ash.Type.apply_constraints/3 enforces one_of, length, minimum, maximum, patterns and empty-string normalisation to nil.
Skipping the second step meant any HTTP client could send a value outside an allowlist or bound. The generated TypeScript client displayed a restricted type, hiding the defect during ordinary development. That type disappears at runtime and does not bind a handwritten caller. When a constraint represents a role, workflow state, sort direction or operation variant, the result can become a privilege or state-machine bypass.
CVE-2026-82732 affects versions from 0.15.0 and scores 6.3. The patch applies constraints after casting. Security tests should exercise raw HTTP rather than relying only on the generated SDK.
CVE-2026-77950 and CVE-2026-82733: two exceptions to safe error policy
In CVE-2026-77950, an application could configure an error handler to redact or suppress a failure. Handlers commonly pattern-match known shapes. An unexpected shape raised FunctionClauseError. The rescue logged a warning and then returned the original, unredacted map. An intention to hide an error became a decision to publish it. The flaw scores 6.3 and affects versions from 0.8.0.
CVE-2026-82733 occurred when a typed-controller handler returned anything other than %Plug.Conn{}. unexpected_return/2 interpolated inspect(value, limit: 50) into the HTTP 500 body. limit: 50 limits elements per collection, not overall sensitivity or size. An error tuple containing %User{}, a changeset or another rich struct could expose password hashes, tokens and tenant identifiers. This path was always active even though raised exceptions elsewhere respected typed_controller_show_raised_errors?. It also scores 6.3 and begins at 0.15.0.
Safe behaviour is fail-closed: a redactor failure must produce a generic message rather than falling back to raw data. Details belong in a controlled log with secret masking and restricted access.
CVE-2026-82731: a typed path parameter without encodeURIComponent
Route generation inserted a :param value into a template string without encodeURIComponent. A parent segment could normalise into another route. ? or # altered path and query boundaries, while a route beginning with a parameter could produce a protocol-relative URL. Fetch could then send the request and configured credentials to another origin.
The query-string path did not share this flaw because URLSearchParams.set encodes its values. Affected versions start at 0.15.0, and CVSS is 2.3. Its local impact can still matter when the client attaches a token. TypeScript checks a value’s compile-time type, not its meaning as a URL segment.
Establishing reachability
Start with the version shipped in the Elixir release. Inventory three surfaces: RPC field selection, typed controllers and generated clients used in browsers or Node. Not every application exposes every feature.
The review should establish:
- whether an anonymous caller selects RPC or typed-struct fields;
- whether responses contain embedded resources and policy-protected fields;
- whether typed-controller arguments depend on
one_of, bounds or regex; - whether an error handler uses partial pattern matching;
- whether a handler can return a term carrying user records or secrets;
- whether generated URLs begin with a parameter and automatically attach credentials.
Upgrade to ash_typescript 0.18.0 or a later compatible release. If rollout must wait, restrict anonymous RPC at the edge, reject unknown field names before the library, enforce argument allowlists server-side, disable detailed errors and avoid affected generated clients for routes whose first segment is controlled.
Detection and safe validation
Monitor BEAM atom count and its growth rate relative to RPC traffic. Search logs for many unknown fields, SystemLimitError, HTTP 500 responses containing struct representations, the original_value key and values outside declared constraints. On clients, detect requests to unexpected origins and mismatches between logical routes and final URLs.
Post-upgrade tests can remain small. A few hundred unique invalid names should be rejected without measurable atom growth; a forbidden field must become nil without original_value; an invalid enum and empty required argument should fail cleanly; a redactor error must return a generic message; and special characters in path parameters must be encoded.
Do not drive a production node toward the atom limit. Trend data and an isolated-node test are sufficient.
Source facts and Breachroad conclusions
Mechanisms, ranges, CVSS ratings and the 0.18.0 fix come from ash_typescript advisories, commits and CNA/NVD records. Inventory, telemetry and test ordering are Breachroad’s conclusions. The sources do not report confirmed active exploitation of this series.
Primary sources
- CVE-2026-74837: atoms from RPC fields
- CVE-2026-77856: atoms from typed structs
- CVE-2026-77950: error-handler failure
- CVE-2026-82730: ForbiddenField disclosure
- CVE-2026-82731: unencoded URL parameter
- CVE-2026-82732: skipped constraints
- CVE-2026-82733: raw term in HTTP 500
- ash_typescript 0.18.0
The boundary between a typed client and its server remains an untrusted-input boundary. Our cybersecurity training teaches teams to test runtime behaviour rather than type declarations alone. A controlled web and API penetration test can verify field policies, RPC, validation and error channels in a specific deployment.


