[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] Prevent unauthenticated password change via new_user_validation

"Espoir Baraka \(@esbarakabigega\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a83134652a20_3818c018913f5@gitlab-sidekiq-low-urgency-cpu-bound-v2-8694b5698b-m6msr.mail>

Espoir Baraka pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
8d978919 by Espoir Baraka at 2026-08-17T15:40:14+02:00
[FIX] Prevent unauthenticated password change via new_user_validation
---
* [FIX] Prevent unauthenticated password change via new_user_validation

Bind new-user password setup to a server-side session marker set after
account validation, instead of trusting the client-supplied flag.

(cherry picked from commit 5b92a8de526556ef3402d6d7e2c03a9b9b48792e)

See merge request tikiwiki/tiki!10909

- - - - -


3 changed files:

- installer/tiki-installer.php
- tiki-change_password.php
- tiki-login_validate.php


Changes:

=====================================
installer/tiki-installer.php
=====================================
@@ -661,7 +661,8 @@ if ($install_step == '9') {
         // sefurl() falls back to the edit url and the admin lands on an empty editor instead of the home page.
         TikiLib::lib('wiki')->createDefaultHomePage();
         TikiLib::lib('unifiedsearch')->rebuild();
-        $u = isset($defaultpass) ? 'tiki-change_password.php?user=admin&oldpass=' . $defaultpass . '&newuser=y' : 'tiki-change_password.php?user=admin&newuser=y';
+        // Always pass oldpass so first admin password setup cannot rely on a forgeable flag
+        $u = 'tiki-change_password.php?user=admin&oldpass=' . urlencode($defaultpass ?? 'admin') . '&newuser=y';
         $tikilib = TikiLib::lib('tiki');
         $tikilib->set_preference('tiki_install_version', $TWV->version);
     } else {


=====================================
tiki-change_password.php
=====================================
@@ -45,12 +45,47 @@ $bruteForceProperties = function () use ($tikilib) {
     return ['ip' => $tikilib->get_ip_address()];
 };
 
-if (isset($_REQUEST["newuser"]) && $_REQUEST["newuser"] == 'y') {
+$pass_confirm = $userlib->getOne('select `pass_confirm` from `users_users` where binary `login`=?', [$user]);
+$must_change_password = ($pass_confirm === 0 || $pass_confirm === null);
+
+/**
+ * New-user password setup is only allowed after a server-side validation step
+ * (email/admin validation) that stored a session marker for this exact user.
+ * Never trust client-supplied new_user_validation / newuser alone.
+ */
+$getPendingNewUserPasswordUser = static function (): ?string {
+    $pending = $_SESSION['pending_new_user_password']['user'] ?? null;
+    if (is_string($pending) && $pending !== '') {
+        return $pending;
+    }
+    $fromValidation = $_SESSION['last_validation']['user'] ?? null;
+    if (is_string($fromValidation) && $fromValidation !== '') {
+        return $fromValidation;
+    }
+    return null;
+};
+
+$pending_new_user = $getPendingNewUserPasswordUser();
+$server_new_user_validation = $must_change_password
+    && $pending_new_user !== null
+    && hash_equals($pending_new_user, (string) $user);
+
+if ($server_new_user_validation) {
+    $smarty->assign('new_user_validation', 'y');
+} elseif (isset($_REQUEST["newuser"]) && $_REQUEST["newuser"] == 'y' && $must_change_password) {
     $smarty->assign('new_user_validation', 'y');
 }
 
 $smarty->assign('userlogin', $_REQUEST["user"]);
-$smarty->assign('oldpass', $_REQUEST["oldpass"]);
+if (
+    empty($_REQUEST['oldpass'])
+    && $server_new_user_validation
+    && ! empty($_SESSION['last_validation']['pass'])
+) {
+    $smarty->assign('oldpass', $_SESSION['last_validation']['pass']);
+} else {
+    $smarty->assign('oldpass', $_REQUEST["oldpass"]);
+}
 $smarty->assign('secure_token', $secure_token);
 
 if (isset($_REQUEST["change"])) {
@@ -75,9 +110,6 @@ if (isset($_REQUEST["change"])) {
     $is_authenticated = false;
     $authenticated_oldpass = null;
     $can_change_password = false;
-    $is_new_user_validation = isset($_REQUEST["new_user_validation"]) && $_REQUEST["new_user_validation"] === 'y';
-    $pass_confirm = $userlib->getOne('select `pass_confirm` from `users_users` where binary `login`=?', [$user]);
-    $must_change_password = ($pass_confirm === 0 || $pass_confirm === null);
 
     // Method 1: Activation code
     if (! empty($_REQUEST['actpass'])) {
@@ -112,11 +144,16 @@ if (isset($_REQUEST["change"])) {
             $is_authenticated = true;
             $can_change_password = true;
             $authenticated_oldpass = $_REQUEST['oldpass'];
+        } elseif ($server_new_user_validation) {
+            // After email validation, provpass may be present in the form but not yet a login hash
+            $is_authenticated = true;
+            $can_change_password = true;
         } else {
             Feedback::error(tra("Invalid old password"));
         }
-    } elseif ($is_new_user_validation) {
-        // Method 4: New user validation (no authentication required - legitimate exception)
+    } elseif ($server_new_user_validation) {
+        // Method 4: New user validation — session-bound after real account validation only
+        $is_authenticated = true;
         $can_change_password = true;
     } elseif ($must_change_password) {
         // Method 5: User must change password - old password is required
@@ -168,8 +205,11 @@ if (isset($_REQUEST["change"])) {
                 $bruteForce->success('change_password', $changePasswordProperties);
             }
 
+            // One-time marker: do not allow reuse of the validation session for another change
+            unset($_SESSION['pending_new_user_password'], $_SESSION['last_validation']);
+
             // Mark reset token as used only after successful password change
-            if (! empty($secure_token) && ! $is_new_user_validation && ! $must_change_password) {
+            if (! empty($secure_token) && ! $server_new_user_validation && ! $must_change_password) {
                 $passwordResetLib = new \Tiki\Lib\Auth\PasswordResetLib();
                 $passwordResetLib->markPasswordResetTokenUsed($user, $secure_token);
             }


=====================================
tiki-login_validate.php
=====================================
@@ -108,6 +108,10 @@ if ($isvalid) {
             if (! empty($_SESSION['last_validation']['pass'])) {
                 $smarty->assign('oldpass', $_SESSION['last_validation']['pass']);
             }
+            $_SESSION['pending_new_user_password'] = [
+                'user' => $_REQUEST['user'],
+                'created' => $tikilib->now,
+            ];
             $smarty->assign('new_user_validation', 'y');
             $smarty->assign('userlogin', $_REQUEST['user']);
             if ($prefs['login_is_email'] === 'y') {



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8d9789194c0f9de3ce50ea82445c59238eaa56ff

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/8d9789194c0f9de3ce50ea82445c59238eaa56ff
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.