Skip to content

Instantly share code, notes, and snippets.

@zachdaniel
Last active October 23, 2024 15:08
Show Gist options
  • Save zachdaniel/e49166b765978c48dfaf998d06df436e to your computer and use it in GitHub Desktop.
Save zachdaniel/e49166b765978c48dfaf998d06df436e to your computer and use it in GitHub Desktop.
Empty, atomic, non-bulk actions, policy bypass for side-effects vulnerability.
# Run this in iex and it will tell you which actions may be affected, and what to look for if any are.
# for the CVE: https://github.com/ash-project/ash_postgres/security/advisories/GHSA-hf59-7rwq-785m
(
:my_otp_app
|> Application.get_env(:ash_domains, [])
|> Enum.flat_map(fn domain ->
domain
|> Ash.Domain.Info.resources()
end)
|> Enum.uniq()
|> Enum.flat_map(fn resource ->
resource
|> Ash.Resource.Info.actions()
|> Enum.filter(&(&1.type == :update))
|> Enum.map(&%{resource: resource, action: &1})
end)
|> Enum.filter(fn resource_action ->
Ash.Resource.Info.data_layer(resource_action.resource) == AshPostgres.DataLayer
&& resource_action.action.type == :update
&& resource_action.action.type.require_atomic?
&& !Enum.empty?(Ash.Resource.Info.authorizers(resource_action.resource))
end)
|> tap(fn resource_actions ->
IO.puts("Checking #{Enum.count(resource_actions)} total update actions using `AshPostgres` data layer, that have at least one authorizer")
end)
|> Enum.reject(fn resource_action ->
changes =
resource_action.action.changes
|> Enum.concat(Ash.Resource.Info.changes(resource_action.resource, resource_action.action.type))
# if there are no changes, then there can be no side effects
if Enum.empty?(changes) do
true
else
# The vulnerability can only occur if the update action can be done atomically.
# We check for changes, notifiers, and non_atomic_attributes that could
changes =
changes
|> Enum.concat(Ash.Resource.Info.validations(resource_action.resource, resource_action.action.type))
|> Enum.map(fn
%{change: {module, _}} ->
module
%{validation: {module, _}} ->
module
end)
|> Enum.reject(fn module ->
!module.atomic?()
end)
notifiers =
resource_action.resource
|> Ash.Resource.Info.notifiers()
|> Enum.filter(fn notifier ->
notifier.requires_original_data?(resource_action.resource, resource_action.action.name)
end)
non_atomic_attributes =
resource_action.action.accept
|> Enum.map(&Ash.Resource.Info.attribute(resource_action.resource, &1))
|> Enum.filter(fn %{type: type} ->
case type do
{:array, {:array, _}} ->
true
{:array, type} when is_atom(type) ->
type = Code.ensure_compiled!(type)
type == Ash.Type.Union || !function_exported?(type, :cast_atomic_array, 2)
type when is_atom(type) ->
Code.ensure_compiled!(type)
type == Ash.Type.Union || !function_exported?(type, :cast_atomic_array, 2)
_ ->
false
end
end)
!resource_action.action.manual && !Enum.any?(changes) && !Enum.any?(notifiers) && !Enum.any?(non_atomic_attributes)
end
end)
|> tap(fn resource_actions ->
IO.puts("#{Enum.count(resource_actions)} of which could be done atomically, and could have a side effect")
end)
|> Enum.reject(fn resource_action ->
resource_action.resource
|> Ash.Resource.Info.attributes()
|> Enum.any?(fn attribute ->
not is_nil(attribute.update_default)
end)
end)
|> tap(fn resource_actions ->
IO.puts("""
#{Enum.count(resource_actions)} of which have no attributes with an update_default,
meaning they will always be updated in an update action).
""")
end)
|> case do
[] ->
IO.puts("No potential vulnerabilities found")
resource_actions ->
actions =
Enum.map_join(resource_actions, "\n", fn %{resource: resource, action: %{name: name}} ->
"- #{inspect(resource)}.#{name}"
end)
"""
The following resource actions *may* be affected.
#{actions}
To determine conclusively, you will need to look at the following:
1. Is there ever a place where you call this action manually, using `Ash.update`. Note that AshGraphql and AshJsonApi are not affected.
2. Is there ever a case where you call the action with zero inputs, and have it produce zero changing fields.
3. If so, could it then produce a side effect. This means you'd have an after_action hook that calls some other resource.
4. If so, does that side effect bypass another resource's policies, i.e using `authorize?: false`.
The only thing that you must do to address this vulnerability is upgrade `ash_postgres`.
"""
end
); nil
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment