Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
563 changes: 286 additions & 277 deletions core/composer.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion core/config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
'Evolution_TemplateProcessor' => EvolutionCMS\Providers\TemplateProcessorServiceProvider::class,
'Evolution_HelperProcessor' => EvolutionCMS\Providers\HelperProcessorServiceProvider::class,
'Evolution_Blade' => EvolutionCMS\Providers\BladeServiceProvider::class,
'Evolution_UserManager' => EvolutionCMS\UserManager\Providers\UserManagerServiceProvider::class,
'Evolution_UserManager' => EvolutionCMS\Providers\PipelineUserManagerServiceProvider::class,
'Evolution_DocumentManager' => EvolutionCMS\DocumentManager\Providers\DocumentManagerServiceProvider::class,
'Evolution_Routing' => EvolutionCMS\Providers\RoutingServiceProvider::class,
'Evolution_Config' => EvolutionCMS\Providers\ConfigServiceProvider::class,
Expand Down
37 changes: 37 additions & 0 deletions core/config/cms/auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Login pipeline
|--------------------------------------------------------------------------
|
| Ordered classes run around each way into a session. Empty by default: the
| pipeline changes nothing until a site or an extra adds pipes to it.
|
| Keys are the entry points — 'login', 'loginById', 'hashLogin' — plus '*',
| which runs for all of them. A second factor belongs under '*': listed only
| under 'login' it is bypassed by a password recovery link or remember-me.
| A flat list is treated as '*'.
|
| Override per key from core/custom/config/cms/auth/pipeline.php.
|
| '*' => [\EvolutionCMS\Auth\Pipes\EnsureNotThrottled::class],
|
*/
'pipeline' => [],

/*
|--------------------------------------------------------------------------
| Login rate limiting
|--------------------------------------------------------------------------
|
| Used by the EnsureNotThrottled pipe: how many attempts per username and IP
| are allowed, and for how many seconds the counter is kept.
|
*/
'throttle' => [
'attempts' => 5,
'decay' => 300,
],
];
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ public function up(): void
$table->increments('id');
$table->string('username')->default('');
$table->string('password')->default('');
$table->string('cachepwd')->default('')->comment('Store new unconfirmed password');
$table->string('cachepwd')->default('')->comment('One-time password recovery token');
$table->dateTime('cachepwd_valid_to')->nullable()->comment('Expiry of the recovery token in cachepwd; NULL = never expires');
$table->string('refresh_token')->nullable();
$table->string('access_token')->nullable();
$table->timestamp('valid_to')->nullable();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

/**
* Gives the password-recovery token in `users.cachepwd` an expiry.
*
* Without it the token is a permanent login link: anybody holding an old recovery
* mail can still use it years later.
*/
class AddCachepwdExpiryToUsers extends Migration {
public function up() {
if (!Schema::hasTable('users') || Schema::hasColumn('users', 'cachepwd_valid_to')) {
return;
}

Schema::table('users', function (Blueprint $table) {
$table->dateTime('cachepwd_valid_to')->nullable()->after('cachepwd');
});

// From here on an empty deadline means "never expires" (pwd_repair_minutes = 0).
// Tokens that already exist have no deadline recorded and would silently become
// eternal, so they are cleared: their owners simply request a new link.
DB::table('users')->where('cachepwd', '<>', '')->update([
'cachepwd' => '',
'cachepwd_valid_to' => null,
]);
}

public function down() {
if (Schema::hasTable('users') && Schema::hasColumn('users', 'cachepwd_valid_to')) {
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('cachepwd_valid_to');
});
}
}
}
2 changes: 1 addition & 1 deletion core/database/seeders/AdminUserTableSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function run(): void

$usersData = [
'username' => $username,
'password' => md5($password),
'password' => evolutionCMS()->getPasswordHash()->HashPassword($password),
'cachepwd' => '',
'refresh_token' => null,
'access_token' => null,
Expand Down
3 changes: 2 additions & 1 deletion core/factory/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
'aliaslistingfolder' => '0',
'check_files_onlogin' => "index.php\n.htaccess\nmanager/index.php\n/core/config/database/connections/default.php",
'use_captcha' => 0,
'pwd_hash_algo' => 0,
'pwd_hash_algo' => 'BCRYPT',
'rb_base_url' => 'assets/',
'resource_tree_node_name' => 'pagetitle',
'udperms_allowroot' => 0,
'failed_login_attempts' => 3,
'blocked_minutes' => 10,
'pwd_repair_minutes' => 60,
'error_reporting' => '1',
'send_errormail' => '0',
'enable_bindings' => 1,
Expand Down
92 changes: 86 additions & 6 deletions core/functions/processors.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,78 @@ function jsAlert($msg)

if (!function_exists('login')) {
/**
* Verify a password against a password_hash()/phpass/crypt hash.
*
* On success the hash is upgraded in place when it no longer matches the algorithm
* configured in the system settings — that is how $P$ (and every future format
* change) migrates without ever knowing the old passwords in bulk.
*
* @param string $username
* @param string $givenPassword
* @param string $dbasePassword
* @return bool
* @throws \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException
*/
function login($username, $givenPassword, $dbasePassword)
{
$modx = evo();
$hasher = $modx->getPasswordHash();

// Nothing recognisable is stored, so no password can ever match. Silently
// answering "wrong password" would lock the account out for good.
if (!$hasher->isUsable($dbasePassword)) {
startPasswordRecovery($username);
}

if (!$hasher->CheckPassword($givenPassword, $dbasePassword)) {
return false;
}

if ($hasher->needsRehash($dbasePassword)) {
updateNewHash($username, $givenPassword);
}

return true;
}
}

if (!function_exists('startPasswordRecovery')) {
/**
* Begin password recovery for an account whose stored password is unusable.
*
* Always throws: the login flow cannot continue, and the thrown message is what the
* user sees. Repeated attempts reuse the outstanding token instead of sending
* another mail.
*
* @param string $username
* @return void
* @throws \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException
*/
function startPasswordRecovery($username)
{
$user = \EvolutionCMS\Models\User::query()
->where('username', $username)
->first();

return $modx->getPasswordHash()->CheckPassword($givenPassword, $dbasePassword);
if (!is_null($user)) {
try {
(new \EvolutionCMS\Services\PasswordRecoveryService())->startAutomaticRecovery($user);
} catch (\Throwable $exception) {
// A failing mailer must not turn into a fatal on the login screen; the
// user still gets told that the password has to be reset.
evo()->logEvent(0, 3, 'Password recovery could not be started: '
. $exception->getMessage(), 'Auth');
}
}

try {
$message = \Lang::get('global.login_processor_password_recovery');
} catch (\Throwable $exception) {
// No lexicon bound (CLI, installer): the reason still has to reach the caller.
$message = 'The stored password of this account cannot be verified.';
}

throw new \EvolutionCMS\Exceptions\PasswordRecoveryRequiredException($message);
}
}

Expand All @@ -133,12 +195,19 @@ function loginV1($internalKey, $givenPassword, $dbasePassword, $username)
$modx->setConfig('pwd_hash_algo', 'UNCRYPT');
}

if ($user_algo !== $modx->getConfig('pwd_hash_algo')) {
$bk_pwd_hash_algo = $modx->getConfig('pwd_hash_algo');
// genV1Hash() reads the algorithm from the config, so it has to be pointed at
// the algorithm this particular hash was made with — and put back afterwards,
// or the setting stays overwritten for the rest of the request.
$bk_pwd_hash_algo = $modx->getConfig('pwd_hash_algo');
if ($user_algo !== $bk_pwd_hash_algo) {
$modx->setConfig('pwd_hash_algo', $user_algo);
}

if ($dbasePassword != $modx->getManagerApi()->genV1Hash($givenPassword, $internalKey)) {
$expected = $modx->getManagerApi()->genV1Hash($givenPassword, $internalKey);

$modx->setConfig('pwd_hash_algo', $bk_pwd_hash_algo);

if (!hash_equals((string) $dbasePassword, (string) $expected)) {
return false;
}

Expand All @@ -160,7 +229,7 @@ function loginMD5($internalKey, $givenPassword, $dbasePassword, $username)
{
$modx = evo();

if ($dbasePassword != md5($givenPassword)) {
if (!hash_equals((string) $dbasePassword, md5($givenPassword))) {
return false;
}
updateNewHash($username, $givenPassword);
Expand All @@ -178,8 +247,19 @@ function updateNewHash($username, $password)
{
$modx = evo();

$hash = $modx->getPasswordHash()->HashPassword($password);

// '*' is the hasher's failure marker. Writing it would replace a working hash
// with one that can never validate, so leave the old one alone instead.
if (!is_string($hash) || $hash === '' || $hash === '*') {
$modx->logEvent(0, 3, 'Password rehash failed for user ' . $username
. '; the existing hash was kept.', 'Auth');

return;
}

$field = [];
$field['password'] = $modx->getPasswordHash()->HashPassword($password);
$field['password'] = $hash;
\EvolutionCMS\Models\User::where('username', $username)->update($field);

}
Expand Down
19 changes: 15 additions & 4 deletions core/includes/define.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@
define('SESSION_COOKIE_NAME', env('SESSION_COOKIE_NAME', genEvoSessionName())); // $site_sessionname
}

define('EVO_CLASS', '\DocumentParser');
if (!defined('EVO_CLASS')) {
define('EVO_CLASS', '\DocumentParser');
}

define('EVO_SITE_HOSTNAMES', '');
if (!defined('EVO_SITE_HOSTNAMES')) {
define('EVO_SITE_HOSTNAMES', '');
}

if (!defined('MGR_DIR')) {
define('MGR_DIR', env('MGR_DIR', 'manager'));
Expand Down Expand Up @@ -165,8 +169,15 @@
throw new RuntimeException('Please, use trailing slash at the end of EVO_SITE_URL');
}

define('EVO_MANAGER_URL', EVO_SITE_URL . MGR_DIR . '/');
define('EVO_SANITIZE_SEED', 'sanitize_seed_' . base_convert(md5(__FILE__), 16, 36));
if (!defined('EVO_MANAGER_URL')) {
define('EVO_MANAGER_URL', EVO_SITE_URL . MGR_DIR . '/');
}

// Must keep its first value: the sanitize helpers in core/functions/preload.php strip
// this seed back out, so a second, different seed would leave the marker in the output.
if (!defined('EVO_SANITIZE_SEED')) {
define('EVO_SANITIZE_SEED', 'sanitize_seed_' . base_convert(md5(__FILE__), 16, 36));
}

if (is_cli()) {
if (!defined('EVO_CLI')) { define('EVO_CLI', true); }
Expand Down
8 changes: 7 additions & 1 deletion core/lang/az/global.php
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,10 @@
$_lang["block_message"] = 'Bu istifadəçi məlumatları saxlandıqdan sonra bloklanacaq!';
$_lang["blocked_minutes_message"] = 'İstifadəçi icazə verilən maksimum uğursuz giriş cəhdlərinin sayına çatdıqda neçə dəqiqə bloklanacağını daxil edin. Bu dəyəri yalnız rəqəmlərlə yazın (vergül, boşluq və s. olmadan).';
$_lang["blocked_minutes_title"] = 'Bloklanma müddəti (dəqiqə)';
$_lang["pwd_repair_minutes_title"] = 'Parol bərpa keçidinin ömrü';
$_lang["pwd_repair_minutes_message"] = 'Parol bərpa keçidi göndərildikdən sonra neçə dəqiqə etibarlı qalır. Keçidlərin heç vaxt bitməməsi üçün 0 daxil edin. Bu dəyəri yalnız rəqəmlə daxil edin (vergül, boşluq və s. olmadan)';
$_lang["forgot_password_email_valid_until"] = 'Bu keçid bir dəfə istifadə oluna bilər və :datetime tarixinədək etibarlıdır.';
$_lang["forgot_password_email_valid_unlimited"] = 'Bu keçid bir dəfə istifadə oluna bilər və müddəti bitmir.';
$_lang["cache_files_deleted"] = 'Aşağıdakı fayllar silindi:';
$_lang["cancel"] = 'Ləğv et';
$_lang["captcha_code"] = 'Təhlükəsizlik kodu';
Expand Down Expand Up @@ -752,7 +756,6 @@
$_lang["image_base_upload_dir_title"] = 'Fayl Brauzer yükləmə kökü';

$_lang["folder"] = 'Qovluq';
$_lang["forgot_password_email_fine_print"] = '* Yuxarıdakı keçid parolunuzu dəyişdirdikdən sonra və ya bu günün sonunda etibarsız olacaq.';
$_lang["forgot_password_email_instructions"] = 'Buradan "Mənim Hesabım" menyusu vasitəsilə parolunuzu dəyişə biləcəksiniz.';
$_lang["forgot_password_email_intro"] = 'Hesabınızın parolunu dəyişdirmək üçün bir sorğu göndərilib.';
$_lang["forgot_password_email_link"] = 'Prosesi tamamlamaq üçün buraya klikləyin.';
Expand Down Expand Up @@ -1466,6 +1469,9 @@

$_lang["login_processor_unknown_user"] = "Daxil edilən istifadəçi adı və ya şifrə yanlışdır!";
$_lang["login_processor_wrong_password"] = "Daxil edilən istifadəçi adı və ya şifrə yanlışdır!";
$_lang["login_processor_password_recovery"] = 'Bu hesabın saxlanılmış parolunu yoxlamaq mümkün deyil. Yeni parol təyin etmək üçün keçid hesabın e-mail ünvanına göndərildi.';
$_lang["login_processor_hash_expired"] = 'Bu parol bərpa keçidi artıq etibarlı deyil. Zəhmət olmasa yenisini tələb edin.';
$_lang["login_processor_throttled"] = 'Həddindən artıq giriş cəhdi. :seconds saniyə sonra yenidən cəhd edin.';
$_lang["login_processor_many_failed_logins"] = "Çox uğursuz giriş cəhdinə görə hesabınız bloklanıb!";
$_lang["login_processor_verified"] = "İstifadəçi doğrulaması tələb olunur!";
$_lang["login_processor_blocked1"] = "Siz bloklandınız və giriş edə bilməzsiniz!";
Expand Down
8 changes: 7 additions & 1 deletion core/lang/be/global.php
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,10 @@
$_lang["block_message"] = 'Гэты карыстальнік будзе заблакаваны пасля захавання дадзеных карыстальніка!';
$_lang["blocked_minutes_message"] = 'Увядзіце колькасць хвілін, на працягу якіх карыстальнік будзе заблакаваны, калі ён дасягне максімальнай колькасці няўдалых спроб уваходу. Увядзіце толькі лічбы (без коскі, прабелаў і г.д.)';
$_lang["blocked_minutes_title"] = 'Заблакаваныя хвіліны';
$_lang["pwd_repair_minutes_title"] = 'Час жыцця спасылкі аднаўлення пароля';
$_lang["pwd_repair_minutes_message"] = 'Колькі хвілін спасылка для аднаўлення пароля застаецца дзейснай пасля адпраўкі. Увядзіце 0, каб спасылкі не мелі тэрміну дзеяння. Калі ласка, увядзіце гэта значэнне як лік (без коскаў, прабелаў і г.д.)';
$_lang["forgot_password_email_valid_until"] = 'Спасылка аднаразовая і дзейсная да :datetime.';
$_lang["forgot_password_email_valid_unlimited"] = 'Спасылка аднаразовая і не мае тэрміну дзеяння.';
$_lang["cache_files_deleted"] = 'На наступныя файлы было наладжана выдаленне:';
$_lang["cancel"] = 'Скасаваць';
$_lang["captcha_code"] = 'Код CAPTCHA';
Expand Down Expand Up @@ -741,7 +745,6 @@
$_lang["image_base_upload_dir_title"] = 'Кореневы каталог загрузкі браўзера файлаў';

$_lang["folder"] = 'Папка';
$_lang["forgot_password_email_fine_print"] = '* Тэрмін дзеяння спасылкі заканчваецца пасля змены пароля або сёння.';
$_lang["forgot_password_email_instructions"] = 'Для змены пароля перайдзіце ў меню «Мой акаўнт».';
$_lang["forgot_password_email_intro"] = 'Быў запыт на змену пароля вашага акаўнта.';
$_lang["forgot_password_email_link"] = 'Націсніце тут, каб завяршыць працэс.';
Expand Down Expand Up @@ -1465,6 +1468,9 @@

$_lang["login_processor_unknown_user"] = "Уведзены няправільны лагін або пароль!";
$_lang["login_processor_wrong_password"] = "Уведзены няправільны лагін або пароль!";
$_lang["login_processor_password_recovery"] = 'Пароль гэтага ўліковага запісу захаваны ў фармаце, які немагчыма праверыць. Спасылка для ўстаноўкі новага пароля адпраўлена на e-mail уліковага запісу.';
$_lang["login_processor_hash_expired"] = 'Спасылка для аднаўлення пароля больш не дзейнічае. Запытайце новую.';
$_lang["login_processor_throttled"] = 'Занадта шмат спроб уваходу. Паўтарыце праз :seconds с.';
$_lang["login_processor_many_failed_logins"] = "З-за занадта вялікай колькасці няўдалых уваходаў вы былі заблакіраваны!";
$_lang["login_processor_verified"] = "Патрабуецца праверка карыстальніка!";
$_lang["login_processor_blocked1"] = "Вы заблакіраваны і не можаце ўвайсці!";
Expand Down
Loading