-
-
Save zachdaniel/e49166b765978c48dfaf998d06df436e to your computer and use it in GitHub Desktop.
Empty, atomic, non-bulk actions, policy bypass for side-effects vulnerability.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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