{"id":"CVE-2026-55736","aliases":["GHSA-f4hc-ppw9-4hhw"],"title":"Ash: Private action arguments can be set by user input via string-keyed params and atomic changesets","summary":"Ash: Private action arguments can be set by user input via string-keyed params and atomic changesets","severity":"medium","cwe":["CWE-915"],"vendor":"ash","product":"ash","ecosystem":"erlang","affected":["ash >= 3.0.0, < 3.29.3"],"patched":["ash 3.29.3"],"published":"2026-09-24","updated":"2026-09-24","sourceUpdated":"2026-09-24T19:53:38Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-f4hc-ppw9-4hhw","references":[{"url":"https://github.com/ash-project/ash/security/advisories/GHSA-f4hc-ppw9-4hhw"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55736"},{"url":"https://github.com/ash-project/ash/commit/d9b3100219b3ea86d73202bf7368c03a7688efea"},{"url":"https://cna.erlef.org/cves/CVE-2026-55736.html"},{"url":"https://github.com/ash-project/ash/releases/tag/v3.29.3"},{"url":"https://osv.dev/vulnerability/EEF-CVE-2026-55736"},{"url":"https://github.com/advisories/GHSA-f4hc-ppw9-4hhw"}],"tags":["ghsa","erlang"],"epss":0.00367,"epssPercentile":0.27757,"ingestedAt":"2026-09-24T20:51:40.264Z","slug":"CVE-2026-55736","body":"## Overview\n\n### Summary\n\nAsh fails to consistently strip private action arguments (those declared with `public?: false`) when a changeset is built from an untrusted parameter map. Private arguments are meant to be set only by trusted server-side code, but a caller who controls the parameters supplied to an action can inject a value for one. Any actor able to submit parameters to an action that defines a private argument can trigger it.\n\n### Details\n\nPrivate arguments (`public?: false`) are meant to be populated internally (e.g. via `Ash.Changeset.set_private_argument/3`) and never accepted from external input. When an action is invoked with a parameter map, Ash should discard keys that name a private argument. The filtering in `lib/ash/changeset/changeset.ex` is incomplete, and the gap differs across the two parameter paths.\n\n**1. Regular path (`for_create`, `for_update`, `for_destroy`).** `cast_params/4` validates keys via `get_action_argument/2`. Its atom-keyed clause filters on `public?`, but the binary-keyed (string) clause does not, so a string key matching a private argument name is accepted and written into `changeset.arguments`. User-supplied parameter maps are string-keyed, making this the reachable case.\n\n**2. Atomic / bulk path (`Ash.Changeset.fully_atomic_changeset/4`).** `atomic_params/4` gates assignment on `has_argument?/2`, whose atom and binary clauses both omit the `public?` check, so private arguments are accepted regardless of key type.\n\n### PoC\n\n1. Define an action with a private argument, e.g. `argument :acting_user_id, :string, public?: false`, and a change that writes it into an attribute.\n2. Build the changeset from a string-keyed map including it, e.g. `Ash.Changeset.for_create(Resource, :place, %{\"item\" => \"book\", \"acting_user_id\" => \"victim-user-id\"})`.\n3. Observe `acting_user_id` is present in `changeset.arguments` and persisted, whereas the same map with atom keys is correctly stripped.\n4. For the atomic path, call `Ash.Changeset.fully_atomic_changeset(Resource, :promote, %{\"acting_user_id\" => \"victim-user-id\"})` (atom or string keys) and observe the private argument is retained either way.\n\n### Impact\n\nAn attacker who can submit parameters to an action that defines a private argument can set that argument to a value of their choosing, overriding data the application intended to control server-side. Where a private argument drives authorization, identity, or record ownership (e.g. `acting_user_id`), this can lead to an integrity violation or privilege escalation.\n\n## Affected packages\n\n- `ash >= 3.0.0, < 3.29.3`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `ash 3.29.3`","depth":"sunlit","depthScore":28,"depthScoreParts":{"impact":27.5,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}