[TikiWiki-commits] [Git][tikiwiki/tiki][29.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 | <6a10b7faeb07a_38192f1c3123@gitlab-sidekiq-low-urgency-cpu-bound-v2-5755d7f9f9-s5q9n.mail> |
Elifeleti Mukisa Dan pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki Commits: 5a1e35f8 by Elifeleti Mukisa Dan at 2026-05-22T20:02:38+00:00 [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 - - - - - 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 ===================================== @@ -632,7 +632,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 ===================================== @@ -1128,6 +1128,124 @@ 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 ===================================== @@ -1202,7 +1202,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; @@ -1210,7 +1215,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 ===================================== @@ -170,7 +170,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']); + $access->redirect($prefs['url_after_validation'], allowExternal: true); } else { $accesslib->redirect($homePageUrl); } ===================================== 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 ===================================== @@ -334,7 +334,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 ===================================== @@ -50,7 +50,7 @@ try { $smarty->assign('create2FaCodeNormalLogin', $create2FaCodeNormalLogin); 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/5a1e35f8a9718102c40fba63191d9f3bab6ba9b8 -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/5a1e35f8a9718102c40fba63191d9f3bab6ba9b8 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