CVE-2022-39355: Improper Authentication (CWE-287)
value: Discourse Patreon enables syncronization between Discourse Groups and Patreon rewards. On sites with Patreon login enabled, an improper authentication vulnerability could be used to take control of a victim's forum account. This vulnerability is patched in commit number 846d012151514b35ce42a1636c7d70f6dcee879e of the discourse-patreon plugin. Out of an abundance of caution, any Discourse accounts which have logged in with an unverified-email Patreon account will be logged out and asked to verify their email address on their next login. As a workaround, disable the patreon integration and log out all users with associated Patreon accounts.
CWE description: When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.
CVSS v3 base score: 9.8
Published: 2022-10-26T20:15Z
Repository: https://github.com/discourse/discourse-patreon
Language: Ruby
Files changed: db/post_migrate/20221025153038_deactivate_unverified_email_patreon_accounts.rb, db/post_migrate/20221026043851_delete_unverified_patreon_user_info.rb, plugin.rb
Commit date: 2022-10-26 19:55:43 +0100
Commit message: SECURITY: Check for email verification status during login (#92)

This commit also includes a migration which will deactivate any users with existing connections to unverified-email Patreon accounts. They will be asked to verify their email on their next login.
Diff: diff --git a/db/post_migrate/20221025153038_deactivate_unverified_email_patreon_accounts.rb b/db/post_migrate/20221025153038_deactivate_unverified_email_patreon_accounts.rb
new file mode 100644
index 0000000..061828e
--- /dev/null
+++ b/db/post_migrate/20221025153038_deactivate_unverified_email_patreon_accounts.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class DeactivateUnverifiedEmailPatreonAccounts < ActiveRecord::Migration[6.1]
+  def up
+    execute <<~SQL
+      UPDATE users SET active = false
+      WHERE users.id IN (
+        SELECT user_id FROM user_associated_accounts
+        WHERE provider_name = 'patreon'
+        AND extra -> 'raw_info' -> 'data' -> 'attributes' ->> 'is_email_verified' = 'false'
+      )
+    SQL
+  end
+
+  def down
+    # noop
+  end
+end
diff --git a/db/post_migrate/20221026043851_delete_unverified_patreon_user_info.rb b/db/post_migrate/20221026043851_delete_unverified_patreon_user_info.rb
new file mode 100644
index 0000000..75c21fe
--- /dev/null
+++ b/db/post_migrate/20221026043851_delete_unverified_patreon_user_info.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class DeleteUnverifiedPatreonUserInfo < ActiveRecord::Migration[6.1]
+  def up
+    execute <<~SQL
+      DELETE FROM user_auth_tokens
+      WHERE user_id IN (
+        SELECT user_id
+        FROM user_associated_accounts
+        WHERE provider_name = 'patreon'
+        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
+      )
+    SQL
+
+    execute <<~SQL
+      UPDATE user_api_keys
+      SET revoked_at = NOW()
+      WHERE user_id IN (
+        SELECT user_id
+        FROM user_associated_accounts
+        WHERE provider_name = 'patreon'
+        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
+      )
+    SQL
+
+    execute <<~SQL
+      UPDATE api_keys
+      SET revoked_at = NOW()
+      WHERE created_by_id IN (
+        SELECT user_id
+        FROM user_associated_accounts
+        WHERE provider_name = 'patreon'
+        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
+      )
+    SQL
+
+    execute <<~SQL
+      DELETE FROM user_associated_accounts
+      WHERE provider_name = 'patreon'
+      AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
+    SQL
+  end
+
+  def down
+    # noop
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index 40daf02..6502889 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -176,10 +176,11 @@ class ::OmniAuth::Strategies::Patreon < ::OmniAuth::Strategies::OAuth2
   }
 
   info do
     {
       email: raw_info['data']['attributes']['email'],
+      email_verified: raw_info['data']['attributes']['is_email_verified'],
       name: raw_info['data']['attributes']['full_name']
     }
   end
 
   extra do
@@ -232,8 +233,12 @@ class Auth::PatreonAuthenticator < Auth::ManagedAuthenticator
   end
 
   def enabled?
     SiteSetting.patreon_login_enabled
   end
+
+  def primary_email_verified?(auth_token)
+    auth_token[:info][:email_verified]
+  end
 end
 
 auth_provider authenticator: Auth::PatreonAuthenticator.new

Fixed code: # frozen_string_literal: true

class DeactivateUnverifiedEmailPatreonAccounts < ActiveRecord::Migration[6.1]
  def up
    execute <<~SQL
      UPDATE users SET active = false
      WHERE users.id IN (
        SELECT user_id FROM user_associated_accounts
        WHERE provider_name = 'patreon'
        AND extra -> 'raw_info' -> 'data' -> 'attributes' ->> 'is_email_verified' = 'false'
      )
    SQL
  end

  def down
    # noop
  end
end
# frozen_string_literal: true

class DeleteUnverifiedPatreonUserInfo < ActiveRecord::Migration[6.1]
  def up
    execute <<~SQL
      DELETE FROM user_auth_tokens
      WHERE user_id IN (
        SELECT user_id
        FROM user_associated_accounts
        WHERE provider_name = 'patreon'
        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
      )
    SQL

    execute <<~SQL
      UPDATE user_api_keys
      SET revoked_at = NOW()
      WHERE user_id IN (
        SELECT user_id
        FROM user_associated_accounts
        WHERE provider_name = 'patreon'
        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
      )
    SQL

    execute <<~SQL
      UPDATE api_keys
      SET revoked_at = NOW()
      WHERE created_by_id IN (
        SELECT user_id
        FROM user_associated_accounts
        WHERE provider_name = 'patreon'
        AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
      )
    SQL

    execute <<~SQL
      DELETE FROM user_associated_accounts
      WHERE provider_name = 'patreon'
      AND COALESCE(JSON_EXTRACT_PATH(extra::json, 'raw_info', 'data', 'attributes', 'is_email_verified')::text, 'false') <> 'true'
    SQL
  end

  def down
    # noop
  end
end
      email_verified: raw_info['data']['attributes']['is_email_verified'],

  def primary_email_verified?(auth_token)
    auth_token[:info][:email_verified]
  end