mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-10-28 14:08:45 -04:00
* auth: rotate session ID on all authenticated transitions (session fixation) The PHP session ID was regenerated on explicit form login, reauth, logout and password change, but NOT when access is (re)established from a remember-me cookie, via HTTP authentication, or on self-registration auto-login (CWE-384), so a fixed session ID could become authenticated. Regenerate the session ID at those transitions too: in FreshRSS_Auth::init when accessControl() first grants access (covers remember-me restore and HTTP auth; skipped for auth_type 'none', which has no authentication boundary), and in the self-registration auto-login path, mirroring the existing form-login idiom. Also enable session.use_strict_mode before session_start as defence in depth so an uninitialised, attacker-supplied session ID is not adopted. * auth: avoid duplicate cookie during session rotation * Code preferences, shorten some comments --------- Co-authored-by: Inverle <inverle@proton.me> Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
296 lines
8.1 KiB
PHP
296 lines
8.1 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This class handles all authentication process.
|
|
*/
|
|
class FreshRSS_Auth {
|
|
/**
|
|
* Determines if user is connected.
|
|
*/
|
|
public const DEFAULT_COOKIE_DURATION = 7_776_000;
|
|
|
|
private static bool $login_ok = false;
|
|
|
|
/**
|
|
* This method initializes authentication system.
|
|
*/
|
|
public static function init(): bool {
|
|
if (isset($_SESSION['REMOTE_USER']) && $_SESSION['REMOTE_USER'] !== FreshRSS_http_Util::httpAuthUser()) {
|
|
//HTTP REMOTE_USER has changed
|
|
self::removeAccess();
|
|
Minz_Session::_param('csrf', false);
|
|
}
|
|
|
|
self::$login_ok = Minz_Session::paramBoolean('loginOk');
|
|
$current_user = Minz_User::name();
|
|
if ($current_user === null) {
|
|
$current_user = FreshRSS_Context::systemConf()->default_user;
|
|
Minz_Session::_params([
|
|
Minz_User::CURRENT_USER => $current_user,
|
|
]);
|
|
}
|
|
|
|
if (self::$login_ok && self::giveAccess()) {
|
|
return self::$login_ok;
|
|
}
|
|
if (self::accessControl()) {
|
|
// Rotate the PHP session ID on the unauthenticated->authenticated transition
|
|
if (FreshRSS_Context::systemConf()->auth_type !== 'none') {
|
|
try {
|
|
Minz_Session::regenerateID('FreshRSS');
|
|
} catch (RuntimeException $e) {
|
|
Minz_Log::error('Session could not be regenerated during access restoration: ' . $e->getMessage());
|
|
}
|
|
}
|
|
if (self::giveAccess()) {
|
|
FreshRSS_UserDAO::touch();
|
|
return self::$login_ok;
|
|
}
|
|
}
|
|
// Be sure all accesses are removed!
|
|
self::removeAccess();
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This method checks if user is allowed to connect.
|
|
*
|
|
* Required session parameters are also set in this method (such as
|
|
* currentUser).
|
|
*
|
|
* @return bool true if user can be connected, false otherwise.
|
|
*/
|
|
private static function accessControl(): bool {
|
|
$auth_type = FreshRSS_Context::systemConf()->auth_type;
|
|
switch ($auth_type) {
|
|
case 'form':
|
|
$credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
|
|
$current_user = '';
|
|
if (isset($credentials[1])) {
|
|
$current_user = trim($credentials[0]);
|
|
Minz_Session::_params([
|
|
Minz_User::CURRENT_USER => $current_user,
|
|
'passwordHash' => trim($credentials[1]),
|
|
'csrf' => false,
|
|
]);
|
|
}
|
|
return $current_user != '';
|
|
case 'http_auth':
|
|
$current_user = FreshRSS_http_Util::httpAuthUser();
|
|
if ($current_user == '') {
|
|
return false;
|
|
}
|
|
$login_ok = FreshRSS_UserDAO::exists($current_user);
|
|
if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) {
|
|
$email = null;
|
|
if (FreshRSS_Context::systemConf()->http_auth_auto_register_email_field !== '' &&
|
|
is_string($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field] ?? null)) {
|
|
$email = $_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field];
|
|
}
|
|
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
|
|
Minz_Translate::init($language);
|
|
$login_ok = FreshRSS_user_Controller::createUser($current_user, $email, '', [
|
|
'language' => $language,
|
|
]);
|
|
}
|
|
if ($login_ok) {
|
|
Minz_Session::_params([
|
|
Minz_User::CURRENT_USER => $current_user,
|
|
'csrf' => false,
|
|
]);
|
|
}
|
|
return $login_ok;
|
|
case 'none':
|
|
return true;
|
|
default:
|
|
// TODO load extension
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gives access to the current user.
|
|
*/
|
|
public static function giveAccess(): bool {
|
|
FreshRSS_Context::initUser();
|
|
if (!FreshRSS_Context::hasUserConf() || !FreshRSS_Context::userConf()->enabled) {
|
|
self::$login_ok = false;
|
|
return false;
|
|
}
|
|
|
|
switch (FreshRSS_Context::systemConf()->auth_type) {
|
|
case 'form':
|
|
self::$login_ok = Minz_Session::paramString('passwordHash') === FreshRSS_Context::userConf()->passwordHash;
|
|
break;
|
|
case 'http_auth':
|
|
$current_user = Minz_User::name() ?? '';
|
|
self::$login_ok = strcasecmp($current_user, FreshRSS_http_Util::httpAuthUser()) === 0;
|
|
break;
|
|
case 'none':
|
|
self::$login_ok = true;
|
|
break;
|
|
default:
|
|
// TODO: extensions
|
|
self::$login_ok = false;
|
|
}
|
|
|
|
Minz_Session::_params([
|
|
'loginOk' => self::$login_ok,
|
|
'REMOTE_USER' => FreshRSS_http_Util::httpAuthUser(),
|
|
]);
|
|
return self::$login_ok;
|
|
}
|
|
|
|
/**
|
|
* Returns if current user has access to the given scope.
|
|
*
|
|
* @param string $scope general (default) or admin
|
|
* @return bool true if user has corresponding access, false else.
|
|
*/
|
|
public static function hasAccess(string $scope = 'general'): bool {
|
|
if (!FreshRSS_Context::hasUserConf()) {
|
|
return false;
|
|
}
|
|
$currentUser = Minz_User::name();
|
|
$isAdmin = FreshRSS_Context::userConf()->is_admin;
|
|
$default_user = FreshRSS_Context::systemConf()->default_user;
|
|
$ok = self::$login_ok;
|
|
switch ($scope) {
|
|
case 'general':
|
|
break;
|
|
case 'admin':
|
|
$ok &= $default_user === $currentUser || $isAdmin;
|
|
break;
|
|
default:
|
|
$ok = false;
|
|
}
|
|
return (bool)$ok;
|
|
}
|
|
|
|
/**
|
|
* Removes all accesses for the current user.
|
|
*/
|
|
public static function removeAccess(): void {
|
|
self::$login_ok = false;
|
|
Minz_Session::_params([
|
|
'loginOk' => false,
|
|
'lastReauth' => false,
|
|
// 'csrf' => false, // Must be refreshed separately
|
|
'REMOTE_USER' => false,
|
|
]);
|
|
|
|
$username = Minz_Request::paramString('user');
|
|
if (!Minz_Request::tokenIsOk()) {
|
|
$username = FreshRSS_Context::systemConf()->default_user;
|
|
}
|
|
Minz_User::change($username);
|
|
|
|
switch (FreshRSS_Context::systemConf()->auth_type) {
|
|
case 'form':
|
|
Minz_Session::_param('passwordHash');
|
|
FreshRSS_FormAuth::deleteCookie();
|
|
break;
|
|
case 'http_auth':
|
|
case 'none':
|
|
// Nothing to do…
|
|
break;
|
|
default:
|
|
// TODO: extensions
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return if authentication is enabled on this instance of FRSS.
|
|
*/
|
|
public static function accessNeedsLogin(): bool {
|
|
return FreshRSS_Context::systemConf()->auth_type !== 'none';
|
|
}
|
|
|
|
/**
|
|
* Return if authentication requires a PHP action.
|
|
*/
|
|
public static function accessNeedsAction(): bool {
|
|
return FreshRSS_Context::systemConf()->auth_type === 'form';
|
|
}
|
|
|
|
public static function csrfToken(): string {
|
|
$csrf = Minz_Session::paramString('csrf');
|
|
if ($csrf == '') {
|
|
$csrf = hash('sha256', FreshRSS_Context::systemConf()->salt . random_bytes(32));
|
|
Minz_Session::_param('csrf', $csrf);
|
|
}
|
|
return $csrf;
|
|
}
|
|
|
|
public static function isCsrfOk(?string $token = null): bool {
|
|
$csrf = Minz_Session::paramString('csrf');
|
|
if ($token === null) {
|
|
$token = is_string($_POST['_csrf'] ?? null) ? $_POST['_csrf'] : '';
|
|
}
|
|
return $token != '' && hash_equals($csrf, $token);
|
|
}
|
|
|
|
public static function needsReauth(): bool {
|
|
$auth_type = FreshRSS_Context::systemConf()->auth_type;
|
|
$reauth_required = FreshRSS_Context::systemConf()->reauth_required;
|
|
$reauth_time = FreshRSS_Context::systemConf()->reauth_time;
|
|
|
|
if (!$reauth_required) {
|
|
return false;
|
|
}
|
|
|
|
$last_reauth = Minz_Session::paramInt('lastReauth');
|
|
|
|
if ($auth_type !== 'none' && time() - $last_reauth > $reauth_time) {
|
|
if ($auth_type === 'http_auth') {
|
|
// TODO: not implemented - just let the user through
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return if user needs reauth and got redirected to login page.
|
|
*
|
|
* @param array{c?: string, a?: string, params?: array<string, mixed>}|null $redirect
|
|
*/
|
|
public static function requestReauth(?array $redirect = null): bool {
|
|
if (self::needsReauth()) {
|
|
if (Minz_Request::paramBoolean('ajax')) {
|
|
// Send 403 and exit instead of redirect with Minz_Error::error()
|
|
header('HTTP/1.1 403 Forbidden');
|
|
exit();
|
|
}
|
|
|
|
$redirect = Minz_Url::serialize($redirect ?? Minz_Request::currentRequest());
|
|
|
|
Minz_Request::forward([
|
|
'c' => 'auth',
|
|
'a' => 'reauth',
|
|
'params' => [
|
|
'r' => $redirect,
|
|
],
|
|
], true);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static function allowAnonymous(): bool {
|
|
$defaultUser = FreshRSS_Context::systemConf()->default_user;
|
|
$requestedUser = Minz_Request::paramString('user');
|
|
return FreshRSS_Context::systemConf()->allow_anonymous &&
|
|
Minz_User::name() === $defaultUser &&
|
|
($requestedUser === '' || $requestedUser === $defaultUser);
|
|
}
|
|
|
|
public static function allowAnonymousRefresh(): bool {
|
|
return FreshRSS_Context::systemConf()->allow_anonymous_refresh;
|
|
}
|
|
}
|