CVE-2019-16764: Insufficient Information (NVD-CWE-noinfo)
value: The use of `String.to_atom/1` in PowAssent is susceptible to denial of service attacks. In `PowAssent.Phoenix.AuthorizationController` a value is fetched from the user provided params, and `String.to_atom/1` is used to convert the binary value to an atom so it can be used to fetch the provider configuration value. This is unsafe as it is user provided data, and can be used to fill up the whole atom table of ~1M which will cause the app to crash.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 5.5
Published: 2019-11-25T17:15Z
Repository: https://github.com/pow-auth/pow_assent
Language: Other
Files changed: CHANGELOG.md, lib/pow_assent/config.ex, lib/pow_assent/plug.ex, test/pow_assent/config_test.exs
Commit date: 2019-11-22 11:21:24 -0800
Commit message: Prevent `String.to_atom/1` calls

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6660e4f..1e66ebd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,13 @@
 # Changelog
 
+## v0.4.4 (TBA)
+
+* [`PowAssent.Plug`] Now uses `String.to_existing_atom/1` in `PowAssent.Plug.providers_for_current_user/1`
+* [`PowAssent.Plug`] Fixed security issue by removing `String.to_atom/1` for user provided binary in `PowAssent.Plug.authorize_url/3` and `PowAssent.Plug.callback/4`
+* [`PowAssent.Config`] `PowAssent.Config.get_provider_config/2` now accepts binary provider
+
 ## v0.4.3 (2019-11-20)
 
 * Removed `:phoenix_html` dependency requirement
 * Added Pow minimum requirement `~> 1.0.15`
 * Use `Pow.Extension.Base` macro for new extension setup
diff --git a/lib/pow_assent/config.ex b/lib/pow_assent/config.ex
index 72702d0..940cd4f 100644
--- a/lib/pow_assent/config.ex
+++ b/lib/pow_assent/config.ex
@@ -48,19 +48,31 @@ defmodule PowAssent.Config do
   def get_providers(config), do: get(config, :providers, [])
 
   @doc """
   Gets the provider configuration from the provided configuration.
   """
-  @spec get_provider_config(t(), atom()) :: t() | no_return
+  @spec get_provider_config(t(), atom() | binary()) :: t() | no_return
   def get_provider_config(config, provider) do
     config
     |> get_providers()
-    |> get(provider)
+    |> get_for_provider(provider)
     |> Kernel.||(raise_error("No provider configuration available for #{provider}."))
     |> add_global_config(config)
   end
 
+  defp get_for_provider(providers_config, provider) when is_atom(provider) do
+    get(providers_config, provider)
+  end
+  defp get_for_provider(providers_config, provider) when is_binary(provider) do
+    Enum.find_value(providers_config, fn {key, value} ->
+      case Atom.to_string(key) do
+        ^provider -> value
+        _any      -> false
+      end
+    end)
+  end
+
   defp add_global_config(provider_config, config) do
     [
       :http_adapter,
       :json_adapter,
       :jwt_adapter
diff --git a/lib/pow_assent/plug.ex b/lib/pow_assent/plug.ex
index c8093b7..737e4ca 100644
--- a/lib/pow_assent/plug.ex
+++ b/lib/pow_assent/plug.ex
@@ -195,11 +195,11 @@ defmodule PowAssent.Plug do
     config = fetch_config(conn)
 
     conn
     |> Pow.Plug.current_user()
     |> get_all_providers_for_user(config)
-    |> Enum.map(&String.to_atom(&1.provider))
+    |> Enum.map(&String.to_existing_atom(&1.provider))
   end
 
   defp get_all_providers_for_user(nil, _config), do: []
   defp get_all_providers_for_user(user, config), do: Operations.all(user, config)
 
@@ -230,11 +230,10 @@ defmodule PowAssent.Plug do
     conn
     |> fetch_config()
     |> get_provider_config(provider, redirect_uri)
   end
   defp get_provider_config(config, provider, redirect_uri) do
-    provider        = String.to_atom(provider)
     config          = Config.get_provider_config(config, provider)
     strategy        = config[:strategy]
     provider_config =
       config
       |> Keyword.delete(:strategy)
diff --git a/test/pow_assent/config_test.exs b/test/pow_assent/config_test.exs
index 4f46892..bada72b 100644
--- a/test/pow_assent/config_test.exs
+++ b/test/pow_assent/config_test.exs
@@ -26,6 +26,16 @@ defmodule PowAssent.ConfigTest do
     end
 
     assert Config.get_provider_config([http_adapter: HTTPAdapater, json_adapter: JSONAdapter, jwt_adapter: JWTAdapter], :provider1) ==
       [http_adapter: HTTPAdapater, json_adapter: JSONAdapter, jwt_adapter: JWTAdapter, a: 1]
   end
+
+  test "get_provider_config/2 with binary provider" do
+    config = [providers: [provider1: [a: 1], provider2: [b: 2]]]
+
+    assert Config.get_provider_config(config, "provider1") == [a: 1]
+
+    assert_raise PowAssent.Config.ConfigError, "No provider configuration available for non_existent.", fn ->
+      refute Config.get_provider_config(config, "non_existent")
+    end
+  end
 end

Vulnerable code:   @spec get_provider_config(t(), atom()) :: t() | no_return
    |> get(provider)
    |> Enum.map(&String.to_atom(&1.provider))
    provider        = String.to_atom(provider)
Fixed code: ## v0.4.4 (TBA)

* [`PowAssent.Plug`] Now uses `String.to_existing_atom/1` in `PowAssent.Plug.providers_for_current_user/1`
* [`PowAssent.Plug`] Fixed security issue by removing `String.to_atom/1` for user provided binary in `PowAssent.Plug.authorize_url/3` and `PowAssent.Plug.callback/4`
* [`PowAssent.Config`] `PowAssent.Config.get_provider_config/2` now accepts binary provider

  @spec get_provider_config(t(), atom() | binary()) :: t() | no_return
    |> get_for_provider(provider)
  defp get_for_provider(providers_config, provider) when is_atom(provider) do
    get(providers_config, provider)
  end
  defp get_for_provider(providers_config, provider) when is_binary(provider) do
    Enum.find_value(providers_config, fn {key, value} ->
      case Atom.to_string(key) do
        ^provider -> value
        _any      -> false
      end
    end)
  end

    |> Enum.map(&String.to_existing_atom(&1.provider))

  test "get_provider_config/2 with binary provider" do
    config = [providers: [provider1: [a: 1], provider2: [b: 2]]]

    assert Config.get_provider_config(config, "provider1") == [a: 1]

    assert_raise PowAssent.Config.ConfigError, "No provider configuration available for non_existent.", fn ->
      refute Config.get_provider_config(config, "non_existent")
    end
  end