CVE-2024-22202: Improper Access Control (CWE-284)
value: phpMyFAQ is an open source FAQ web application for PHP 8.1+ and MySQL, PostgreSQL and other databases. phpMyFAQ's user removal page allows an attacker to spoof another user's detail, and in turn make a compelling phishing case for removing another user's account. The front-end of this page doesn't allow changing the form details, an attacker can utilize a proxy to intercept this request and submit other data. Upon submitting this form, an email is sent to the administrator informing them that this user wants to delete their account. An administrator has no way of telling the difference between the actual user wishing to delete their account or the attacker issuing this for an account they do not control. This issue has been patched in version 3.2.5.
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
CVSS v3 base score: 6.5
Published: 2024-02-05T20:15Z
Repository: https://github.com/thorsten/phpMyFAQ
Language: PHP
Files changed: phpmyfaq/api.service.php, phpmyfaq/assets/themes/default/templates/request-removal.html, phpmyfaq/request-removal.php
Commit date: 2024-01-12 12:15:57 +0100
Commit message: fix: added missing check on user to request removal

Diff: diff --git a/phpmyfaq/api.service.php b/phpmyfaq/api.service.php
index bfa6bc49aa..1b7848f5ce 100644
--- a/phpmyfaq/api.service.php
+++ b/phpmyfaq/api.service.php
@@ -976,20 +976,34 @@ switch ($action) {
             $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
             $response->setData(['error' => Translation::get('ad_msg_noauth')]);
             break;
         }
 
+        $userId = Filter::filterVar($postData['userId'], FILTER_VALIDATE_INT);
         $author = trim((string) Filter::filterVar($postData['name'], FILTER_SANITIZE_SPECIAL_CHARS));
         $loginName = trim((string) Filter::filterVar($postData['loginname'], FILTER_SANITIZE_SPECIAL_CHARS));
         $email = trim((string) Filter::filterVar($postData['email'], FILTER_VALIDATE_EMAIL));
         $question = trim((string) Filter::filterVar($postData['question'], FILTER_SANITIZE_SPECIAL_CHARS));
 
         // If e-mail address is set to optional
         if (!$faqConfig->get('main.optionalMailAddress') && is_null($email)) {
             $email = $faqConfig->getAdminEmail();
         }
 
+        // Validate User ID, Username and email
+        $user = new User($faqConfig);
+        if (
+            !$user->getUserById($userId) ||
+            $userId !== $user->getUserId() ||
+            $loginName !== $user->getLogin() ||
+            $email !== $user->getUserData('email')
+        ) {
+            $response->setStatusCode(Response::HTTP_BAD_REQUEST);
+            $response->setData(['error' => Translation::get('ad_user_error_loginInvalid')]);
+            break;
+        }
+
         if (!empty($author) && !empty($email) && !empty($question) && $stopWords->checkBannedWord($question)) {
             $question = sprintf(
                 "%s %s\n%s %s\n%s %s\n\n %s",
                 Translation::get('ad_user_loginname'),
                 $loginName,
diff --git a/phpmyfaq/assets/themes/default/templates/request-removal.html b/phpmyfaq/assets/themes/default/templates/request-removal.html
index f7280be346..ad5dacb114 100644
--- a/phpmyfaq/assets/themes/default/templates/request-removal.html
+++ b/phpmyfaq/assets/themes/default/templates/request-removal.html
@@ -16,11 +16,12 @@
       <div id="faqs"></div>
     </div>
   </div>
 
   <form id="formValues" action="#" method="post" class="needs-validation" novalidate>
-    <input type="hidden" name="lang" id="lang" value="{{ lang }}" />
+    <input type="hidden" name="lang" id="lang" value="{{ lang }}">
+    <input type="hidden" name="userId" id="userId" value="{{ userId }}">
     {{ csrf }}
 
     <div class="row mb-2">
       <label class="col-sm-3 form-control-label" for="name">{{ ad_user_loginname }}</label>
       <div class="col-sm-9">
diff --git a/phpmyfaq/request-removal.php b/phpmyfaq/request-removal.php
index 8c3f04a28f..79c5ef5963 100644
--- a/phpmyfaq/request-removal.php
+++ b/phpmyfaq/request-removal.php
@@ -45,10 +45,11 @@ $template->parse(
         'msgNewContentName' => Translation::get('msgNewContentName'),
         'msgNewContentMail' => Translation::get('msgNewContentMail'),
         'ad_user_loginname' => Translation::get('ad_user_loginname'),
         'csrf' => Token::getInstance()->getTokenInput('request-removal'),
         'lang' => $Language->getLanguage(),
+        'userId' => $user->getUserId(),
         'defaultContentMail' => ($user->getUserId() > 0) ? $user->getUserData('email') : '',
         'defaultContentName' =>
             ($user->getUserId() > 0) ? Strings::htmlentities($user->getUserData('display_name')) : '',
         'defaultLoginName' => ($user->getUserId() > 0) ? Strings::htmlentities($user->getLogin()) : '',
         'msgMessage' => Translation::get('msgMessage'),

Vulnerable code:     <input type="hidden" name="lang" id="lang" value="{{ lang }}" />
Fixed code:         $userId = Filter::filterVar($postData['userId'], FILTER_VALIDATE_INT);
        // Validate User ID, Username and email
        $user = new User($faqConfig);
        if (
            !$user->getUserById($userId) ||
            $userId !== $user->getUserId() ||
            $loginName !== $user->getLogin() ||
            $email !== $user->getUserData('email')
        ) {
            $response->setStatusCode(Response::HTTP_BAD_REQUEST);
            $response->setData(['error' => Translation::get('ad_user_error_loginInvalid')]);
            break;
        }

    <input type="hidden" name="lang" id="lang" value="{{ lang }}">
    <input type="hidden" name="userId" id="userId" value="{{ userId }}">
        'userId' => $user->getUserId(),