[TikiWiki-commits] [Git][tikiwiki/tiki][27.x] [BP][FIX] 2FA login: replace plaintext password in session with one-time token
"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a10e5251eacd_3819259460846@gitlab-sidekiq-low-urgency-cpu-bound-v2-d7f87744c-4nwwv.mail> |
Elifeleti Mukisa Dan pushed to branch 27.x at Tiki Wiki CMS Groupware / Tiki Commits: f01c32e4 by Elifeleti Mukisa Dan at 2026-05-22T23:16:07+00:00 [BP][FIX] 2FA login: replace plaintext password in session with one-time token --- * [BP][FIX] 2FA login: replace plaintext password in session with one-time token --- * Revert "[BP][FIX] Prevent open redirect via unvalidated user-supplied URLs" This reverts commit 7a543df9fcf2a74da9a925ae3173362f7111335d. * [BP][FIX] 2FA login: replace plaintext password in session with one-time token --- * [FIX] 2FA login: replace plaintext password in session with one-time token --- * [FIX] 2FA login: replace plaintext password in session with one-time token (cherry picked from commit 2dd82889edba45ea9d91708c4f971c313dd79e5c) 2dd82889 [FIX] 2FA login: replace plaintext password in session with one-time token Co-authored-by: Danelif <[email protected]> See merge request tikiwiki/tiki!10294 (cherry picked from commit 5c8daa413e019687021d6b11d0c9e99f3eea5274) 79bc2b2c [FIX] 2FA login: replace plaintext password in session with one-time token Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!10315 See merge request tikiwiki/tiki!10327 See merge request tikiwiki/tiki!10343 - - - - - 11 changed files: - installer/tiki-installer.php - lib/core/CustomRoute/CustomRoute.php - lib/core/Services/User/Controller.php - lib/tikiaccesslib.php - tiki-change_password.php - tiki-channel.php - tiki-index.php - tiki-login.php - tiki-login_scr.php - tiki-payment.php - tiki-wikiplugin_edit.php Changes: ===================================== installer/tiki-installer.php ===================================== @@ -602,7 +602,7 @@ if ($install_step == '9') { if (empty($_REQUEST['multi'])) { $userlib->user_logout($user, false, $u); // logs out then redirects to home page or $u } else { - $access->redirect('http://' . $_REQUEST['multi'] . $tikiroot . $u, allowExternal: true); // send to the selected multitiki + $access->redirect('http://' . $_REQUEST['multi'] . $tikiroot . $u); // send to the selected multitiki } exit; } ===================================== lib/core/CustomRoute/CustomRoute.php ===================================== @@ -45,7 +45,7 @@ class CustomRoute { $access = TikiLib::lib('access'); if ($redirect = $route->getRedirectPath($path)) { - $access->redirect($redirect, allowExternal: true); + $access->redirect($redirect); } else { $access->display_error($path, tra("Page cannot be found"), '404'); } ===================================== lib/core/Services/User/Controller.php ===================================== @@ -1129,124 +1129,6 @@ class Services_User_Controller } } - public function actionLocalTimezoneSync($input) - { - global $user, $tikilib; - $access = TikiLib::lib('access'); - $clientTz = $input->client_timezone->text(); - $userPreferenceTz = $tikilib->get_user_preference($user, 'display_timezone', ''); - $action = $input->timezone_action->text(); - - if ($action === 'never') { - if ($tikilib->set_preference('user_localtimezonesync', 'n')) { - $tikilib->set_user_preference($user, 'localtimezonesync', 'n'); - } - $access->redirect($_SERVER['HTTP_REFERER']); - return []; - } - - if (! $this->isValidTimezone($clientTz)) { - Feedback::error(tr("Invalid detected timezone.")); - $access->redirect($_SERVER['HTTP_REFERER']); - return []; - } - - // Determine effective timezone deterministically - $effectiveTz = ''; - - // Temporary session timezone (highest priority) - if (! empty($_SESSION['temp_timezone']) && $this->isValidTimezone($_SESSION['temp_timezone'])) { - $effectiveTz = $_SESSION['temp_timezone']; - } elseif (! empty($userPreferenceTz) && $this->isValidTimezone($userPreferenceTz)) { // User preference - $effectiveTz = $userPreferenceTz; - } else { // Fallback to detected/system timezone - $effectiveTz = $clientTz; - } - - if ($action === 'switch') { - $tikilib->set_user_preference($user, 'display_timezone', $clientTz); - - unset($_SESSION['temp_timezone']); - - $userPreferenceTz = $clientTz; - $effectiveTz = $clientTz; - } elseif ($action === 'temporary') { - $_SESSION["temp_timezone"] = $clientTz; - $effectiveTz = $clientTz; - } - - // Determine if the client timezone and effective (user/system) timezone are different. - // If they differ by name, we check if they are functionally equivalent - // meaning their UTC offsets are the same throughout the year. - // - // This avoids showing misleading "timezone synchronization" notification when the - // timezones are actually the same in behavior but have different names (e.g. "America/Toronto" vs "EST"). - // - // This logic improves UX by not bothering users with unnecessary alerts - // when the functional result is the same - $different = false; - // In detect mode, effective timezone always follows client - if (empty($userPreferenceTz)) { - $effectiveTz = $clientTz; - } - // $clientTz is already validated above - if (! empty($clientTz)) { - if ($clientTz !== $effectiveTz) { - $different = true; - try { - $tzClient = new DateTimeZone($clientTz); - $tzEffective = new DateTimeZone($effectiveTz); - $year = date('Y'); - $functionallySame = true; - - for ($month = 1; $month <= 12; $month++) { - $dateUTC = new DateTime("$year-$month-15 12:00:00", new DateTimeZone('UTC')); - $offsetClient = $tzClient->getOffset($dateUTC); - $offsetEffective = $tzEffective->getOffset($dateUTC); - - if ($offsetClient !== $offsetEffective) { - $functionallySame = false; - break; - } - } - if ($functionallySame) { - $different = false; - } - } catch (Exception $e) { - Feedback::error("TimezoneSync Error: " . $e->getMessage()); - $different = false; - } - } - } - - $result = [ - 'different' => $different, - 'preferedTimezone' => $userPreferenceTz, - 'clientTimezone' => $clientTz, - 'effectiveTimezone' => $effectiveTz, - ]; - - if ($action === 'switch' || $action === 'temporary') { - return $this->redirectAndReturn($result); - } - - return $result; - } - - private function isValidTimezone(string $timezone): bool - { - return in_array($timezone, DateTimeZone::listIdentifiers(), true); - } - - private function redirectAndReturn($data = []): array - { - $referer = $_SERVER['HTTP_REFERER'] ?? ''; - if ($referer !== '') { - TikiLib::lib('access')->redirect($referer); - } - return $data; - } - private function removeUsers(array $users, $page = false, $trackerIds = [], $files = false) { global $user; ===================================== lib/tikiaccesslib.php ===================================== @@ -1200,12 +1200,7 @@ class TikiAccessLib extends TikiLib * @param int $code HTTP code * @param string $msgtype Type of message which determines styling (e.g., success, error, warning, etc.) */ - /** - * @param bool $allowExternal Pass true only when the URL originates from a trusted source - * (admin-configured preference, inter-tiki setup, OAuth flow, etc.). - * Never pass true for URLs that are user-supplied at request time. - */ - public function redirect($url = '', $msg = '', $code = 302, $msgtype = '', bool $allowExternal = false) + public function redirect($url = '', $msg = '', $code = 302, $msgtype = '') { global $prefs; @@ -1213,23 +1208,7 @@ class TikiAccessLib extends TikiLib return; } - // Validate URL: reject absolute URLs pointing to a different host to prevent open redirects. - // $allowExternal must be explicitly set to true by callers that have already validated or - // trust the URL source (e.g. admin prefs, inter-tiki, OAuth). User-supplied URLs must - // always go through this check. - if (! $allowExternal && $url !== '') { - $parsed = parse_url($url); - if (! empty($parsed['host'])) { - $allowedHost = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''; - // Strip port from allowed host for comparison - $allowedHost = strtolower(explode(':', $allowedHost)[0]); - $targetHost = strtolower($parsed['host']); - if ($targetHost !== $allowedHost) { - // Off-site redirect — fall back to the configured home page - $url = $prefs['tikiIndex']; - } - } - } + // TODO: Validate URL if ($url == '') { $url = $prefs['tikiIndex']; } ===================================== tiki-change_password.php ===================================== @@ -195,7 +195,7 @@ if (isset($_REQUEST["change"])) { $wizardlib->onLogin($user, $homePageUrl, $force); $accesslib = TikiLib::lib('access'); if (! empty($prefs['url_after_validation']) && ! empty($_REQUEST['new_user_validation'])) { - $access->redirect($prefs['url_after_validation'], allowExternal: true); + $access->redirect($prefs['url_after_validation']); } else { $accesslib->redirect($homePageUrl); } ===================================== tiki-channel.php ===================================== @@ -83,5 +83,5 @@ foreach ($calls as $call) { } if (isset($_REQUEST['return_uri'])) { - $access->redirect($_REQUEST['return_uri']); + header("Location: {$_REQUEST['return_uri']}"); } ===================================== tiki-index.php ===================================== @@ -310,7 +310,7 @@ if (empty($info) && ! ($user && $prefs['feature_wiki_userpage'] == 'y' && strcas } if (! $isprefixed && ! empty($prefs['url_anonymous_page_not_found']) && empty($user)) { - $access->redirect($prefs['url_anonymous_page_not_found'], allowExternal: true); + $access->redirect($prefs['url_anonymous_page_not_found']); } if ($user && $prefs['feature_wiki_userpage'] == 'y' && strcasecmp($prefs['feature_wiki_userpage_prefix'], $page) == 0) { ===================================== tiki-login.php ===================================== @@ -62,6 +62,19 @@ if ($prefs['session_silent'] == 'y') { session_start(); } +if ($prefs['auth_webauthn_enabled'] === 'y' && ! empty($_SESSION['webauthn_user'])) { + $_REQUEST['user'] = $_SESSION['webauthn_user']; + $_REQUEST['pass'] = ! empty($_REQUEST['pass']) ? trim($_REQUEST['pass']) : $userlib->genPass(); +} + +$twoFaPrevalidated = false; +if ($prefs['twoFactorAuth'] === 'y' && ! empty($_SESSION['tiki_2fa_token']) && ! empty($_SESSION['tiki_2fa_username'])) { + $_REQUEST['user'] = $_SESSION['tiki_2fa_username']; + $twoFaPrevalidated = true; + unset($_SESSION['tiki_2fa_token']); + unset($_SESSION['tiki_2fa_username']); +} + // Remember where user is logging in from and send them back later; using session variable for those of us who use WebISO services // Note that login from will always be a complete URL (http://...) if (! isset($_SESSION['loginfrom']) && isset($_SERVER['HTTP_REFERER']) && ! preg_match('|/login|', $_SERVER['HTTP_REFERER']) && ! preg_match('|logout|', $_SERVER['HTTP_REFERER'])) { @@ -279,7 +292,14 @@ if ( } } else { // Verify user is valid - $ret = $userlib->validate_user($requestedUser, $pass); + if ($twoFaPrevalidated) { + // Password was already validated before the 2FA prompt; use a token instead of re-checking + $ret = [true, $requestedUser, null, null]; + } elseif ($prefs['auth_webauthn_enabled'] === 'y' && ! empty($_SESSION['webauthn_user']) && $_SESSION['webauthn_user'] === $requestedUser) { + $ret = [$userlib->update_lastlogin($requestedUser), $requestedUser, USER_VALID]; + } else { + $ret = $userlib->validate_user($requestedUser, $pass); + } if (count($ret) == 3) { $ret[] = null; } @@ -302,14 +322,54 @@ if ( } } } elseif ($isvalid) { - $twoFactorSecret = $userlib->get_2_factor_secret($requestedUser); - if ($prefs['twoFactorAuth'] == 'y' && ! empty($twoFactorSecret) && ! $userlib->validate_two_factor($twoFactorSecret, $_REQUEST["twoFactorAuthCode"], $requestedUser)) { - $error = TWO_FA_INCORRECT; - $isvalid = false; - $smarty->assign('twoFactorForm', 'y'); - } else { - $isdue = $userlib->is_due($requestedUser, $method); - $user = $requestedUser; + try { + $requireMfa = false; + $twoFactorSecret = null; + + if (($prefs['twoFactorAuth'] ?? 'n') === 'y') { + $requireMfa = TwoFactorAuth::isMFARequired($requestedUser); + $twoFactorSecret = TwoFactorAuth::get2FactorSecret($requestedUser); + } + + if ( + ($prefs['twoFactorAuth'] ?? 'n') === 'y' + && isset($_REQUEST['login_mode']) + && $_REQUEST['login_mode'] == 'popup' + && $requireMfa + && ! empty($twoFactorSecret) + ) { + $_SESSION['tiki_2fa_token'] = bin2hex(random_bytes(32)); + $_SESSION['tiki_2fa_username'] = $_REQUEST['user']; + $params = '&create2FaCodeNormalLogin&tiki_username=' . urlencode($_REQUEST['user']); + header('Location: ' . $base_url . 'tiki-login_scr.php?twoFactorForm' . $params); + exit; + } + + if (($prefs['twoFactorAuth'] ?? 'n') === 'y' && $requireMfa && ! empty($twoFactorSecret)) { + $twoFactorAuth = TwoFactorAuth::getTwoFactorAuth(); + $is2FaPass = $twoFactorAuth->validateCode($requestedUser, $_REQUEST['twoFactorAuthCode']); + if (! $is2FaPass) { + $error = TWO_FA_INCORRECT; + $smarty->assign('showTwoFactorForm', 'y'); + $_SESSION['tiki_2fa_token'] = bin2hex(random_bytes(32)); + $_SESSION['tiki_2fa_username'] = $_REQUEST['user']; + $smarty->assign('create2FaCodeNormalLogin', "y"); + $smarty->assign('error_login', $error); + $smarty->assign('mid', 'tiki-login.tpl'); + $smarty->display('tiki.tpl'); + exit; + } + } + + if ($isvalid) { + if ($requireMfa && ($prefs['twoFactorAuth'] ?? 'n') === 'y') { + $userlib->updateLastMFADate($requestedUser); + } + $isdue = $userlib->is_due($requestedUser, $method); + $user = $requestedUser; + } + } catch (TwoFactorAuthException $e) { + Feedback::errorAndDie($e->getMessage(), \Laminas\Http\Response::STATUS_CODE_409); } } } ===================================== tiki-login_scr.php ===================================== @@ -20,7 +20,7 @@ if (isset($_REQUEST["twoFactorForm"])) { $smarty->assign('twoFactorForm', $twoFactorForm); if ($prefs['login_autologin'] == 'y' && $prefs['login_autologin_redirectlogin'] == 'y' && ! empty($prefs['login_autologin_redirectlogin_url'])) { - $access->redirect($prefs['login_autologin_redirectlogin_url'], allowExternal: true); + $access->redirect($prefs['login_autologin_redirectlogin_url']); } if (isset($_REQUEST['clearmenucache'])) { ===================================== tiki-payment.php ===================================== @@ -132,7 +132,7 @@ if (isset($_GET['tx'])) { && isset($prefs['payment_paypal_pdt_redirect']) && $prefs['payment_paypal_pdt_redirect'] ) { - $access->redirect($prefs['payment_paypal_pdt_redirect'] . '?invoice=' . $invoice, allowExternal: true); + $access->redirect($prefs['payment_paypal_pdt_redirect'] . '?invoice=' . $invoice); } } } @@ -185,7 +185,8 @@ if (isset($_POST['manual_amount'], $_POST['invoice']) && preg_match('/^\d+(\.\d{ ] ); if (isset($_POST['returnurl'])) { - $access->redirect($_POST['returnurl'], allowExternal: true); + header('Location: ' . $_POST['returnurl']); + exit; } $access->redirect('tiki-payment.php?invoice=' . $_POST['invoice'], tra('Manual payment entered.')); ===================================== tiki-wikiplugin_edit.php ===================================== @@ -14,5 +14,5 @@ trigger_error(tr('Note, deprecated file tiki-wikiplugin_edit.php, code moved to TikiLib::lib('service')->render('plugin', 'replace', $jitPost); -// Use the validated redirect helper to guard against open-redirect via a forged Referer header. -TikiLib::lib('access')->redirect($_SERVER['HTTP_REFERER'] ?? ''); +header("Location: {$_SERVER['HTTP_REFERER']}"); +exit; View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/f01c32e46f1d00b355b459cffdbc597746c8cd4c -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/f01c32e46f1d00b355b459cffdbc597746c8cd4c 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