CVE-2025-32782
Description
Ash Authentication provides authentication for the Ash framework. The confirmation flow for account creation currently uses a GET request triggered by clicking a link sent via email. Some email clients and security tools (e.g., Outlook, virus scanners, and email previewers) may automatically follow these links, unintentionally confirming the account. This allows an attacker to register an account using another user’s email and potentially have it auto-confirmed by the victim’s email client. This does not allow attackers to take over or access existing accounts or private data. It is limited to account confirmation of new accounts only. This vulnerability is fixed in 4.7.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ash_authenticationHex | < 4.7.0 | 4.7.0 |
Patches
2d9b0ca8fc1da99ea38977fd4improvement: mitigate medium-sev security issue for confirmation emails (#968)
14 files changed · +120 −11
documentation/dsls/DSL-AshAuthentication.AddOn.Confirmation.md+18 −0 modified@@ -70,6 +70,23 @@ so via the `AshAuthentication.Strategy` protocol. ...> user.confirmed_at >= one_second_ago() true +## Usage with AshAuthenticationPhoenix + +If you are using `AshAuthenticationPhoenix`, and have `require_interaction?` set to `true`, +which you very much should, then you will need to add a `confirm_route` to your router. This +is placed in the same location as `auth_routes`, and should be provided the user and the +strategy name. For example: + +```elixir +# Remove this if you do not want to use the confirmation strategy +confirm_route( + MyApp.Accounts.User, + :confirm_new_user, + auth_routes_prefix: "/auth", + overrides: [MyApp.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] +) +``` + ## Plugs Confirmation provides a single endpoint for the `:confirm` phase. If you wish @@ -112,6 +129,7 @@ User confirmation flow | [`sender`](#authentication-add_ons-confirmation-sender){: #authentication-add_ons-confirmation-sender .spark-required} | `(any, any, any -> any) \| module` | | How to send the confirmation instructions to the user. | | [`token_lifetime`](#authentication-add_ons-confirmation-token_lifetime){: #authentication-add_ons-confirmation-token_lifetime } | `pos_integer \| {pos_integer, :days \| :hours \| :minutes \| :seconds}` | `{3, :days}` | How long should the confirmation token be valid. If no unit is provided, then hours is assumed. | | [`prevent_hijacking?`](#authentication-add_ons-confirmation-prevent_hijacking?){: #authentication-add_ons-confirmation-prevent_hijacking? } | `boolean` | `true` | Whether or not to prevent upserts over unconfirmed uers. See [the confirmation guide](/documentation/topics/confirmation.md) for more. | +| [`require_interaction?`](#authentication-add_ons-confirmation-require_interaction?){: #authentication-add_ons-confirmation-require_interaction? } | `boolean` | `false` | Whether or not to require user interaction to confirm. If true, the confirmation URLs are changed to a `POST` request, and AshAuthenticationPhoenix will show a button to confirm when the page is visited | | [`confirmed_at_field`](#authentication-add_ons-confirmation-confirmed_at_field){: #authentication-add_ons-confirmation-confirmed_at_field } | `atom` | `:confirmed_at` | The name of the field to store the time that the last confirmation took place. Created if it does not exist. | | [`confirm_on_create?`](#authentication-add_ons-confirmation-confirm_on_create?){: #authentication-add_ons-confirmation-confirm_on_create? } | `boolean` | `true` | Generate and send a confirmation token when a new resource is created. Triggers when a create action is executed _and_ one of the monitored fields is being set. | | [`confirm_on_update?`](#authentication-add_ons-confirmation-confirm_on_update?){: #authentication-add_ons-confirmation-confirm_on_update? } | `boolean` | `true` | Generate and send a confirmation token when a resource is changed. Triggers when an update action is executed _and_ one of the monitored fields is being set. |
documentation/tutorials/confirmation.md+2 −0 modified@@ -97,6 +97,7 @@ defmodule MyApp.Accounts.User do monitor_fields [:email] confirm_on_create? true confirm_on_update? false + require_interaction? true sender MyApp.Accounts.User.Senders.SendNewUserConfirmationEmail end end @@ -221,6 +222,7 @@ defmodule MyApp.Accounts.User do confirm_on_create? false confirm_on_update? true confirm_action_name :confirm_change + require_interaction? true sender MyApp.Accounts.User.Senders.SendEmailChangeConfirmationEmail end end
.formatter.exs+1 −0 modified@@ -93,6 +93,7 @@ spark_locals_without_parens = [ request_action_name: 1, request_password_reset_action_name: 1, require_confirmed_with: 1, + require_interaction?: 1, require_token_presence_for_authentication?: 1, resettable: 0, resettable: 1,
lib/ash_authentication/add_ons/confirmation/dsl.ex+6 −0 modified@@ -53,6 +53,12 @@ defmodule AshAuthentication.AddOn.Confirmation.Dsl do doc: "Whether or not to prevent upserts over unconfirmed uers. See [the confirmation guide](/documentation/topics/confirmation.md) for more." ], + require_interaction?: [ + type: :boolean, + default: false, + doc: + "Whether or not to require user interaction to confirm. If true, the confirmation URLs are changed to a `POST` request, and AshAuthenticationPhoenix will show a button to confirm when the page is visited" + ], confirmed_at_field: [ type: :atom, doc:
lib/ash_authentication/add_ons/confirmation.ex+18 −0 modified@@ -69,6 +69,23 @@ defmodule AshAuthentication.AddOn.Confirmation do ...> user.confirmed_at >= one_second_ago() true + ## Usage with AshAuthenticationPhoenix + + If you are using `AshAuthenticationPhoenix`, and have `require_interaction?` set to `true`, + which you very much should, then you will need to add a `confirm_route` to your router. This + is placed in the same location as `auth_routes`, and should be provided the user and the + strategy name. For example: + + ```elixir + # Remove this if you do not want to use the confirmation strategy + confirm_route( + MyApp.Accounts.User, + :confirm_new_user, + auth_routes_prefix: "/auth", + overrides: [MyApp.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] + ) + ``` + ## Plugs Confirmation provides a single endpoint for the `:confirm` phase. If you wish @@ -90,6 +107,7 @@ defmodule AshAuthentication.AddOn.Confirmation do confirm_on_update?: true, prevent_hijacking?: true, confirmed_at_field: :confirmed_at, + require_interaction?: false, inhibit_updates?: true, monitor_fields: [], auto_confirm_actions: [],
lib/ash_authentication/add_ons/confirmation/plug.ex+27 −3 modified@@ -3,7 +3,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Plug do Handlers for incoming OAuth2 HTTP requests. """ - alias AshAuthentication.{AddOn.Confirmation, Strategy} + alias AshAuthentication.{AddOn.Confirmation, Info, Strategy} + alias AshAuthentication.Errors.InvalidToken alias Plug.Conn import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2] import Ash.PlugHelpers, only: [get_actor: 1, get_tenant: 1, get_context: 1] @@ -16,8 +17,14 @@ defmodule AshAuthentication.AddOn.Confirmation.Plug do opts = opts(conn) result = - strategy - |> Strategy.action(:confirm, conn.params, opts) + case params(strategy, conn.params) do + {:ok, params} -> + strategy + |> Strategy.action(:confirm, params, opts) + + :error -> + {:error, InvalidToken.exception(type: :confirmation)} + end conn |> store_authentication_result(result) @@ -27,4 +34,21 @@ defmodule AshAuthentication.AddOn.Confirmation.Plug do [actor: get_actor(conn), tenant: get_tenant(conn), context: get_context(conn) || %{}] |> Enum.reject(&is_nil(elem(&1, 1))) end + + defp params(strategy, params) when strategy.require_interaction? do + case params do + %{"confirm" => _} -> + {:ok, params} + + params -> + strategy.resource + |> Info.authentication_subject_name!() + |> to_string() + |> then(&Map.fetch(params, &1)) + end + end + + defp params(_strategy, params) do + {:ok, params} + end end
lib/ash_authentication/add_ons/confirmation/strategy.ex+3 −1 modified@@ -28,7 +28,9 @@ defimpl AshAuthentication.Strategy, for: AshAuthentication.AddOn.Confirmation do @doc false @spec method_for_phase(Confirmation.t(), phase) :: Strategy.http_method() - def method_for_phase(_, _), do: :get + def method_for_phase(strategy, _phase) when strategy.require_interaction?, do: :post + + def method_for_phase(_strategy, _phase), do: :get @doc false @spec routes(Confirmation.t()) :: [Strategy.route()]
lib/ash_authentication/add_ons/confirmation/transformer.ex+37 −0 modified@@ -32,6 +32,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do strategy.confirm_action_name, &build_confirm_action(&1, strategy) ), + :ok <- warn_on_require_interaction(strategy), :ok <- validate_confirm_action(dsl_state, strategy), {:ok, dsl_state} <- maybe_build_attribute( @@ -71,6 +72,42 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do end end + defp warn_on_require_interaction(strategy) when strategy.require_interaction? do + :ok + end + + defp warn_on_require_interaction(strategy) do + bypassing_error? = + Application.get_env( + :ash_authentication_phoenix, + :bypass_require_interaction_for_confirmation?, + false + ) + + if bypassing_error? do + :ok + else + {:error, + DslError.exception( + path: [:authentication, :add_ons, :confirmation], + message: """ + `require_interaction?` must be set to true on the + #{inspect(strategy.name)} confirmation for #{inspect(strategy.resource)}. + Without it, confirmation links use a `GET` endpoint for confirmation. Some + email clients and security tools (e.g., Outlook, virus scanners, and email + previewers) may automatically follow these links, unintentionally + confirming the account. This allows an attacker to register an account + using another user’s email and potentially have it auto-confirmed by the + victim’s email client. + + See the security advisory for more information: + + https://github.com/team-alembic/ash_authentication/security/advisories/GHSA-3988-q8q7-p787 + """ + )} + end + end + defp validate_monitor_fields(_dsl_state, %{monitor_fields: []}), do: {:error,
lib/mix/tasks/ash_authentication.add_strategy.ex+1 −0 modified@@ -318,6 +318,7 @@ if Code.ensure_loaded?(Igniter) do monitor_fields [:email] confirm_on_create? true confirm_on_update? false + require_interaction? true confirmed_at_field :confirmed_at auto_confirm_actions [:sign_in_with_magic_link, :reset_password_with_token] sender #{inspect(sender)}
test/ash_authentication/add_ons/confirmation/plug_test.exs+3 −3 modified@@ -31,7 +31,7 @@ defmodule AshAuthentication.AddOn.Confirmation.PlugTest do } assert {_conn, {:error, error}} = - :get + :post |> conn("/", params) |> Plug.confirm(strategy) |> Helpers.get_authentication_result() @@ -47,7 +47,7 @@ defmodule AshAuthentication.AddOn.Confirmation.PlugTest do } assert {_conn, {:error, error}} = - :get + :post |> conn("/", params) |> Plug.confirm(strategy) |> Helpers.get_authentication_result() @@ -73,7 +73,7 @@ defmodule AshAuthentication.AddOn.Confirmation.PlugTest do } assert {_conn, {:ok, confirmed_user}} = - :get + :post |> conn("/", params) |> Plug.confirm(strategy) |> Helpers.get_authentication_result()
test/support/example_multi_tenant/user.ex+1 −0 modified@@ -152,6 +152,7 @@ defmodule ExampleMultiTenant.User do confirmation :confirm do monitor_fields [:username] inhibit_updates? true + require_interaction? true sender fn _user, token, opts -> username =
test/support/example_multi_tenant/user_with_register_magic_link.ex+1 −2 modified@@ -57,8 +57,7 @@ defmodule ExampleMultiTenant.UserWithRegisterMagicLink do inhibit_updates? false confirm_on_create? true confirm_on_update? true - # in a real setup, you should have this - # but we don't for testing purposes + require_interaction? true auto_confirm_actions [:sign_in_with_magic_link] sender fn user, _, _ ->
test/support/example/user.ex+1 −0 modified@@ -179,6 +179,7 @@ defmodule Example.User do confirmation :confirm do monitor_fields [:username] inhibit_updates? true + require_interaction? true sender fn _user, token, opts -> username =
test/support/example/user_with_register_magic_link.ex+1 −2 modified@@ -55,8 +55,7 @@ defmodule Example.UserWithRegisterMagicLink do inhibit_updates? false confirm_on_create? true confirm_on_update? true - # in a real setup, you should have this - # but we don't for testing purposes + require_interaction? true auto_confirm_actions [:sign_in_with_magic_link] sender fn user, _, _ ->
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.