[TikiWiki-commits] [Git][tikiwiki/tiki][27.x] [BP][FIX] Prevent open redirect via unvalidated user-supplied URLs
"Elifeleti Mukisa Dan \(@Danelif\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a10de484114_381925bc20e7@gitlab-sidekiq-low-urgency-cpu-bound-v2-d7f87744c-rwgcp.mail> |
Elifeleti Mukisa Dan pushed to branch 27.x at Tiki Wiki CMS Groupware / Tiki Commits: e796e346 by Elifeleti Mukisa Dan at 2026-05-22T22:46:49+00:00 [BP][FIX] Prevent open redirect via unvalidated user-supplied URLs --- * [BP][FIX] Prevent open redirect via unvalidated user-supplied URLs --- * [BP][FIX] Prevent open redirect via unvalidated user-supplied URLs --- * [FIX] Prevent open redirect via unvalidated user-supplied URLs --- * [FIX] Prevent open redirect via unvalidated user-supplied URLs (cherry picked from commit b240ff127703bcd698638f7b7cfdd48d9e9259ae) b240ff12 [FIX] Prevent open redirect via unvalidated user-supplied URLs Co-authored-by: Danelif <[email protected]> See merge request tikiwiki/tiki!10292 (cherry picked from commit 6688f26db7b2188579e63651a555b4d4c140e9a3) 899120e2 [FIX] Prevent open redirect via unvalidated user-supplied URLs Co-authored-by: Elifeleti Mukisa Dan <[email protected]> See merge request tikiwiki/tiki!10314 See merge request tikiwiki/tiki!10325 See merge request tikiwiki/tiki!10340 - - - - - 10 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_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); // send to the selected multitiki + $access->redirect('http://' . $_REQUEST['multi'] . $tikiroot . $u, allowExternal: true); // 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); + $access->redirect($redirect, allowExternal: true); } else { $access->display_error($path, tra("Page cannot be found"), '404'); } ===================================== lib/core/Services/User/Controller.php ===================================== @@ -1129,7 +1129,125 @@ class Services_User_Controller } } - private function removeUsers(array $users, $page = false, $trackerIds = [], $files = false, $referer = false) + 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; foreach ($users as $deleteuser) { ===================================== lib/tikiaccesslib.php ===================================== @@ -1200,7 +1200,12 @@ class TikiAccessLib extends TikiLib * @param int $code HTTP code * @param string $msgtype Type of message which determines styling (e.g., success, error, warning, etc.) */ - public function redirect($url = '', $msg = '', $code = 302, $msgtype = '') + /** + * @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) { global $prefs; @@ -1208,7 +1213,23 @@ class TikiAccessLib extends TikiLib return; } - // TODO: Validate URL + // 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']; + } + } + } if ($url == '') { $url = $prefs['tikiIndex']; } ===================================== tiki-change_password.php ===================================== @@ -129,6 +129,79 @@ if (isset($_REQUEST["change"])) { } else { $accesslib->redirect($homePageUrl); } + + // Only proceed if password change is allowed + if ($can_change_password) { + // Validate password change operation + $validation_errors = false; + if ($_REQUEST["pass"] != $_REQUEST["passAgain"]) { + Feedback::error(tra("The passwords do not match")); + $validation_errors = true; + } + // Check password policy + $polerr = $userlib->check_password_policy($_REQUEST["pass"]); + if (strlen($polerr) > 0) { + Feedback::error($polerr); + $validation_errors = true; + } + // Also check if new password matches current password hash + $current_hash = $userlib->getOne('select `hash` from `users_users` where binary `login`=?', [$user]); + if (! empty($current_hash) && password_verify($_REQUEST["pass"], $current_hash)) { + Feedback::error(tra("You can not use the same password again")); + $validation_errors = true; + } + // Validate email if provided + if (isset($_REQUEST['email'])) { + if (empty($_REQUEST['email']) || ! validate_email($_REQUEST['email'], $prefs['validateEmail'])) { + Feedback::error(tra('Your email could not be validated; make sure your email is correct')); + $validation_errors = true; + } + } + + // Only proceed with password change if validation passed + if (! $validation_errors) { + // Perform password change operation + if (isset($_REQUEST['email']) && ! empty($_REQUEST['email'])) { + $userlib->change_user_email_only($user, $_REQUEST['email']); + } + $res = $userlib->change_user_password($user, $_REQUEST["pass"]); + if ($res && $prefs['pass_history_management'] === 'y') { + $userlib->addPasswordHistory($user, $_REQUEST["pass"]); + } + + // Mark reset token as used only after successful password change + if (! empty($secure_token) && ! $is_new_user_validation && ! $must_change_password) { + $passwordResetLib = new \Tiki\Lib\Auth\PasswordResetLib(); + $passwordResetLib->markPasswordResetTokenUsed($user, $secure_token); + } + + // Handle encryption if enabled + if ($prefs['feature_user_encryption'] === 'y' && ! empty($authenticated_oldpass)) { + $cryptlib = TikiLib::lib('crypt'); + $cryptlib->onChangeUserPassword($authenticated_oldpass, $_REQUEST["pass"]); + } + + // Login user as part of the change operation + $userlib->update_expired_groups(); + $loginlib = TikiLib::lib('login'); + $loginlib->activateSession($user); + $logslib->add_log('login', 'logged from change_password', $user, '', '', $tikilib->now); + if ($jitRequest->oldpass->text() !== 'admin') { + include TIKI_PATH . '/lib/setup/default_homepage.php'; + } + $homePageUrl = $prefs['tikiIndex']; + $wizardlib = TikiLib::lib('wizard'); + $force = $user == 'admin'; + $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); + } else { + $accesslib->redirect($homePageUrl); + } + } + } + // If authentication failed or validation failed, fall through to display the form } // Display the template ===================================== tiki-channel.php ===================================== @@ -83,5 +83,5 @@ foreach ($calls as $call) { } if (isset($_REQUEST['return_uri'])) { - header("Location: {$_REQUEST['return_uri']}"); + $access->redirect($_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']); + $access->redirect($prefs['url_anonymous_page_not_found'], allowExternal: true); } if ($user && $prefs['feature_wiki_userpage'] == 'y' && strcasecmp($prefs['feature_wiki_userpage_prefix'], $page) == 0) { ===================================== 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']); + $access->redirect($prefs['login_autologin_redirectlogin_url'], allowExternal: true); } 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); + $access->redirect($prefs['payment_paypal_pdt_redirect'] . '?invoice=' . $invoice, allowExternal: true); } } } @@ -185,8 +185,7 @@ if (isset($_POST['manual_amount'], $_POST['invoice']) && preg_match('/^\d+(\.\d{ ] ); if (isset($_POST['returnurl'])) { - header('Location: ' . $_POST['returnurl']); - exit; + $access->redirect($_POST['returnurl'], allowExternal: true); } $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); -header("Location: {$_SERVER['HTTP_REFERER']}"); -exit; +// Use the validated redirect helper to guard against open-redirect via a forged Referer header. +TikiLib::lib('access')->redirect($_SERVER['HTTP_REFERER'] ?? ''); View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e796e346af28adca93eee2391b465e027d518811 -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/e796e346af28adca93eee2391b465e027d518811 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