From f45b546f11897028b63307aedc7db4e7fd58a368 Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Tue, 29 Sep 2026 15:22:46 -0400 Subject: [PATCH 1/2] fix(FOUR-33567): block users after failed login attempts and prevent 502 on lockout - only cache a positive result in Setting::readyToUseSettingsDatabase() so settings are not permanently unavailable in Octane when evaluated before tenant resolution - replace gettext _() with Laravel __() in login, 2FA, password expiry, and script executor flows to avoid php-fpm SIGSEGV on macOS (502 Bad Gateway) - add regression test for account lockout after too many failed login attempts https://processmaker.atlassian.net/browse/FOUR-33567 --- .../Api/ScriptExecutorController.php | 2 +- .../Http/Controllers/Auth/LoginController.php | 2 +- .../Auth/TwoFactorAuthController.php | 7 +++-- .../Middleware/VerifyChangePasswordNeeded.php | 4 +-- ProcessMaker/Models/Setting.php | 19 +++++++----- .../TwoFactorAuthNotification.php | 6 ++-- tests/Feature/AuthTest.php | 29 ++++++++++++++++++- 7 files changed, 51 insertions(+), 18 deletions(-) diff --git a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php index 7627c9d7a4..3f9304e5d7 100644 --- a/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php +++ b/ProcessMaker/Http/Controllers/Api/ScriptExecutorController.php @@ -257,7 +257,7 @@ public function delete(Request $request, ScriptExecutor $scriptExecutor, ScriptM exec($cmd, $out, $return); if ($return !== 0) { - throw ValidationException::withMessages(['delete' => _('Error removing image.') . " {$cmd} " . implode("\n", $out)]); + throw ValidationException::withMessages(['delete' => __('Error removing image.') . " {$cmd} " . implode("\n", $out)]); } } diff --git a/ProcessMaker/Http/Controllers/Auth/LoginController.php b/ProcessMaker/Http/Controllers/Auth/LoginController.php index ccdffe7a09..38b39fc9b6 100644 --- a/ProcessMaker/Http/Controllers/Auth/LoginController.php +++ b/ProcessMaker/Http/Controllers/Auth/LoginController.php @@ -415,7 +415,7 @@ function () use ($user) { protected function throwLockedLoginResponse() { throw ValidationException::withMessages([ - $this->username() => [_('Account locked after too many failed attempts. Contact administrator.')], + $this->username() => [__('Account locked after too many failed attempts. Contact administrator.')], ]); } diff --git a/ProcessMaker/Http/Controllers/Auth/TwoFactorAuthController.php b/ProcessMaker/Http/Controllers/Auth/TwoFactorAuthController.php index ed50fda849..266d40faa5 100644 --- a/ProcessMaker/Http/Controllers/Auth/TwoFactorAuthController.php +++ b/ProcessMaker/Http/Controllers/Auth/TwoFactorAuthController.php @@ -76,7 +76,7 @@ public function validateTwoFactorAuthCode(Request $request) // If empty code return error message if (empty($code)) { // Set error message - session()->put(self::TFA_ERROR, _('Invalid code.')); + session()->put(self::TFA_ERROR, __('Invalid code.')); // Return to 2fa page return redirect()->route('2fa'); @@ -98,7 +98,7 @@ public function validateTwoFactorAuthCode(Request $request) $route = 'login'; } else { // Set error message - session()->put(self::TFA_ERROR, _('Invalid code.')); + session()->put(self::TFA_ERROR, __('Invalid code.')); // Return to 2fa page $route = '2fa'; @@ -153,7 +153,7 @@ public function testSettings(Request $request) if (count($enabled) === 0) { $message = [ 'status' => 'error', - 'message' => __('The two-step method must be selected.') + 'message' => __('The two-step method must be selected.'), ]; $status = 500; } @@ -237,6 +237,7 @@ public static function check2faByGroups() { try { $user = Auth::user(); + return $user->in2FAGroupOrIndependent(); } catch (Exception $e) { session()->put(self::TFA_ERROR, $e->getMessage()); diff --git a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php index 1305992f8e..8aa1a75f06 100644 --- a/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php +++ b/ProcessMaker/Http/Middleware/VerifyChangePasswordNeeded.php @@ -12,7 +12,7 @@ class VerifyChangePasswordNeeded * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param Closure $next * @return mixed */ public function handle($request, Closure $next) @@ -23,7 +23,7 @@ public function handle($request, Closure $next) if ($this->checkPasswordExpiration()) { // Set the error message - session()->put('login-error', _('Your password has expired.')); + session()->put('login-error', __('Your password has expired.')); // Redirect to change password screen return redirect()->route('password.change'); diff --git a/ProcessMaker/Models/Setting.php b/ProcessMaker/Models/Setting.php index 87395b44d8..e40748b0d2 100644 --- a/ProcessMaker/Models/Setting.php +++ b/ProcessMaker/Models/Setting.php @@ -535,15 +535,20 @@ public function getPrometheusMetricLabel(): string public static function readyToUseSettingsDatabase() { - if (!self::$readyToUseSettingsDatabase) { - self::$readyToUseSettingsDatabase = - app('tenant-resolved') && - self::databaseAvailable() && - self::redisAvailable() && - self::settingsTableExists(); + if (self::$readyToUseSettingsDatabase) { + return true; + } + + $ready = app('tenant-resolved') && + self::databaseAvailable() && + self::redisAvailable() && + self::settingsTableExists(); + + if ($ready) { + self::$readyToUseSettingsDatabase = true; } - return self::$readyToUseSettingsDatabase; + return $ready; } private static function databaseAvailable() diff --git a/ProcessMaker/Notifications/TwoFactorAuthNotification.php b/ProcessMaker/Notifications/TwoFactorAuthNotification.php index 2b36e3953a..35204ba566 100644 --- a/ProcessMaker/Notifications/TwoFactorAuthNotification.php +++ b/ProcessMaker/Notifications/TwoFactorAuthNotification.php @@ -19,7 +19,7 @@ class TwoFactorAuthNotification extends Notification /** * Create a new notification instance. * - * @param \ProcessMaker\Models\User $user + * @param User $user * @param string $code */ public function __construct(User $user, string $code) @@ -43,12 +43,12 @@ public function via($notifiable) * Get the mail representation of the notification. * * @param mixed $notifiable - * @return \Illuminate\Notifications\Messages\MailMessage + * @return MailMessage */ public function toMail($notifiable) { return (new MailMessage) - ->subject(_('Security Code')) + ->subject(__('Security Code')) ->greeting($this->user->username) ->line(__('This is your security code: :code', ['code' => $this->code])); } diff --git a/tests/Feature/AuthTest.php b/tests/Feature/AuthTest.php index baa02cd488..ac2df31358 100644 --- a/tests/Feature/AuthTest.php +++ b/tests/Feature/AuthTest.php @@ -74,7 +74,6 @@ public function testLoginWithUnknownUser() } /** - * * Test logout flow */ public function testLogoutStandard() @@ -91,4 +90,32 @@ public function testLogoutStandard() $response = $this->get(route('home')); $response->assertRedirect('/login'); } + + public function testUserIsBlockedAfterTooManyFailedLoginAttempts() + { + config(['password-policies.login_attempts' => 3]); + + $user = User::factory()->create([ + 'username' => 'blocktest', + 'password' => Hash::make('correct-password'), + 'status' => 'ACTIVE', + ]); + + for ($i = 0; $i < 3; $i++) { + $this->post('login', [ + 'username' => 'blocktest', + 'password' => 'wrong-password', + ])->assertSessionHasErrors('username'); + } + + $response = $this->post('login', [ + 'username' => 'blocktest', + 'password' => 'wrong-password', + ]); + $response->assertSessionHasErrors([ + 'username' => 'Account locked after too many failed attempts. Contact administrator.', + ]); + + $this->assertEquals('BLOCKED', $user->fresh()->status); + } } From 05ac8844168c1a7b60eb72d32000830f521e2c4b Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Tue, 29 Sep 2026 16:04:00 -0400 Subject: [PATCH 2/2] fix(FOUR-33567): prevent 502 on login lockout by replacing gettext _() with __() gettext _() causes php-fpm SIGSEGV on macOS when throwing the account locked response after too many failed login attempts. The user was being set to BLOCKED in the database, but the UI returned 502 Bad Gateway instead of the lockout message. Replace _() with Laravel __() in login, 2FA, password expiry, and script executor flows. Add a regression test for account lockout after exceeding the configured login attempt limit. https://processmaker.atlassian.net/browse/FOUR-33567 --- ProcessMaker/Models/Setting.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/ProcessMaker/Models/Setting.php b/ProcessMaker/Models/Setting.php index e40748b0d2..87395b44d8 100644 --- a/ProcessMaker/Models/Setting.php +++ b/ProcessMaker/Models/Setting.php @@ -535,20 +535,15 @@ public function getPrometheusMetricLabel(): string public static function readyToUseSettingsDatabase() { - if (self::$readyToUseSettingsDatabase) { - return true; - } - - $ready = app('tenant-resolved') && - self::databaseAvailable() && - self::redisAvailable() && - self::settingsTableExists(); - - if ($ready) { - self::$readyToUseSettingsDatabase = true; + if (!self::$readyToUseSettingsDatabase) { + self::$readyToUseSettingsDatabase = + app('tenant-resolved') && + self::databaseAvailable() && + self::redisAvailable() && + self::settingsTableExists(); } - return $ready; + return self::$readyToUseSettingsDatabase; } private static function databaseAvailable()