CVE-2021-38725: Improper Restriction of Excessive Authentication Attempts (CWE-307)
value: Fuel CMS 1.5.0 has a brute force vulnerability in fuel/modules/fuel/controllers/Login.php
CWE description: The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
CVSS v3 base score: 5.3
Published: 2021-09-09T15:15Z
Repository: https://github.com/daylightstudio/FUEL-CMS
Language: PHP
Files changed: fuel/modules/fuel/controllers/Login.php
Commit date: 2021-08-09 08:55:09 -0700
Commit message: fix: for issue #581

Diff: diff --git a/fuel/modules/fuel/controllers/Login.php b/fuel/modules/fuel/controllers/Login.php
index 5ca4a6c56..852deb266 100644
--- a/fuel/modules/fuel/controllers/Login.php
+++ b/fuel/modules/fuel/controllers/Login.php
@@ -109,36 +109,12 @@ class Login extends CI_Controller {
 							redirect($this->fuel->config('login_redirect'));
 						}
 					}
 					else
 					{
-						// check if they are no longer in the locked out state and reset variables
-						if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) > (int)$this->fuel->config('seconds_to_unlock'))
-						{
-							$user_data['failed_login_attempts'] = 0;
-							$this->session->unset_userdata('failed_login_timer');
-							unset($user_data['failed_login_timer']);
-						}
-						else
-						{
-							// add to the number of attempts if it's an invalid login'
-							$num_attempts = (!isset($user_data['failed_login_attempts'])) ? 0 : $user_data['failed_login_attempts'] + 1;
-							$user_data['failed_login_attempts'] = $num_attempts;
-						}
+						$this->check_login_attempts($user_data, 'username');
 
-						// check if they should be locked out
-						if (isset($user_data['failed_login_attempts']) AND $user_data['failed_login_attempts'] >= (int)$this->fuel->config('num_logins_before_lock') -1)
-						{
-							$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
-							$user_data['failed_login_timer'] = time();
-							$this->fuel->logs->write(lang('auth_log_account_lockout', $this->input->post('user_name', TRUE), $this->input->ip_address()), 'debug');
-						}
-						else
-						{
-							$this->fuel_users_model->add_error(lang('error_invalid_login'));
-							$this->fuel->logs->write(lang('auth_log_failed_login', $this->input->post('user_name', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
-						}
 					}
 				}
 				else
 				{
 					$this->fuel_users_model->add_error(lang('error_empty_user_pwd'));
@@ -186,18 +162,30 @@ class Login extends CI_Controller {
 	{
 		if ( ! $this->fuel->config('allow_forgotten_password')) show_404();
 
 		$this->js_controller_params['method'] = 'add_edit';
 
+		$session_key = $this->fuel->auth->get_session_namespace();
+
+		$user_data = $this->session->userdata($session_key);
+
 		if ( ! empty($_POST))
 		{
-			if ($this->input->post('email'))
+			if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) < (int)$this->fuel->config('seconds_to_unlock'))
+			{
+ 				$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
+				$user_data['failed_login_timer'] = time();
+			}
+			elseif ($this->input->post('email'))
 			{
 				$user = $this->fuel_users_model->find_one_array(array('email' => $this->input->post('email')));
 
 				if ( ! empty($user['email']))
 				{
+					// reset failed login attempts
+					$user_data['failed_login_timer'] = 0;
+
 					// This generates and saves a token to the user model, returns the token string.
 					$token = $this->fuel_users_model->get_reset_password_token($user['email']);
 
 					if ($token !== FALSE)
 					{
@@ -229,12 +217,14 @@ class Login extends CI_Controller {
 						$this->fuel_users_model->add_error(lang('error_invalid_email'));
 					}
 				}
 				else
 				{
-					$this->fuel_users_model->add_error(lang('error_invalid_email'));
+					$this->check_login_attempts($user_data, 'email');
 				}
+
+				$this->session->set_userdata($session_key, $user_data);
 			}
 			else
 			{
 				$this->fuel_users_model->add_error(lang('error_empty_email'));
 			}
@@ -258,10 +248,47 @@ class Login extends CI_Controller {
 
 		$this->load_view('pwd_reset', $vars);
 
 	}
 
+	protected function check_login_attempts(&$user_data, $field)
+	{
+		if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) > (int)$this->fuel->config('seconds_to_unlock'))
+		{
+			$user_data['failed_login_attempts'] = 0;
+			$this->session->unset_userdata('failed_login_timer');
+			unset($user_data['failed_login_timer']);
+		}
+		else
+		{
+			// add to the number of attempts if it's an invalid login'
+			$num_attempts = (!isset($user_data['failed_login_attempts'])) ? 0 : $user_data['failed_login_attempts'] + 1;
+			$user_data['failed_login_attempts'] = $num_attempts;
+		}
+
+		// check if they should be locked out
+		if (isset($user_data['failed_login_attempts']) AND $user_data['failed_login_attempts'] >= (int)$this->fuel->config('num_logins_before_lock') -1)
+		{
+			$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
+			$user_data['failed_login_timer'] = time();
+			$this->fuel->logs->write(lang('auth_log_account_lockout', $this->input->post($field, TRUE), $this->input->ip_address()), 'debug');
+		}
+		else
+		{
+			if ($field == 'email')
+			{
+				$this->fuel_users_model->add_error(lang('error_invalid_email'));
+				$this->fuel->logs->write(lang('error_invalid_email', $this->input->post('email', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
+			}
+			else
+			{
+				$this->fuel_users_model->add_error(lang('error_invalid_login'));
+				$this->fuel->logs->write(lang('auth_log_failed_login', $this->input->post('user_name', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
+			}
+		}
+	}
+
 	// THIS HANDLES A POST REQUEST FOR USER SETTING A NEW PASSWORD
 	public function reset_password() 
 	{
 		$token = $this->uri->segment(4);
 

Vulnerable code: 						// check if they are no longer in the locked out state and reset variables
						if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) > (int)$this->fuel->config('seconds_to_unlock'))
						{
							$user_data['failed_login_attempts'] = 0;
							$this->session->unset_userdata('failed_login_timer');
							unset($user_data['failed_login_timer']);
						}
						else
						{
							// add to the number of attempts if it's an invalid login'
							$num_attempts = (!isset($user_data['failed_login_attempts'])) ? 0 : $user_data['failed_login_attempts'] + 1;
							$user_data['failed_login_attempts'] = $num_attempts;
						}
						// check if they should be locked out
						if (isset($user_data['failed_login_attempts']) AND $user_data['failed_login_attempts'] >= (int)$this->fuel->config('num_logins_before_lock') -1)
						{
							$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
							$user_data['failed_login_timer'] = time();
							$this->fuel->logs->write(lang('auth_log_account_lockout', $this->input->post('user_name', TRUE), $this->input->ip_address()), 'debug');
						}
						else
						{
							$this->fuel_users_model->add_error(lang('error_invalid_login'));
							$this->fuel->logs->write(lang('auth_log_failed_login', $this->input->post('user_name', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
						}
			if ($this->input->post('email'))
					$this->fuel_users_model->add_error(lang('error_invalid_email'));
Fixed code: 						$this->check_login_attempts($user_data, 'username');
		$session_key = $this->fuel->auth->get_session_namespace();

		$user_data = $this->session->userdata($session_key);

			if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) < (int)$this->fuel->config('seconds_to_unlock'))
			{
 				$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
				$user_data['failed_login_timer'] = time();
			}
			elseif ($this->input->post('email'))
					// reset failed login attempts
					$user_data['failed_login_timer'] = 0;

					$this->check_login_attempts($user_data, 'email');

				$this->session->set_userdata($session_key, $user_data);
	protected function check_login_attempts(&$user_data, $field)
	{
		if (isset($user_data['failed_login_timer']) AND (time() - $user_data['failed_login_timer']) > (int)$this->fuel->config('seconds_to_unlock'))
		{
			$user_data['failed_login_attempts'] = 0;
			$this->session->unset_userdata('failed_login_timer');
			unset($user_data['failed_login_timer']);
		}
		else
		{
			// add to the number of attempts if it's an invalid login'
			$num_attempts = (!isset($user_data['failed_login_attempts'])) ? 0 : $user_data['failed_login_attempts'] + 1;
			$user_data['failed_login_attempts'] = $num_attempts;
		}

		// check if they should be locked out
		if (isset($user_data['failed_login_attempts']) AND $user_data['failed_login_attempts'] >= (int)$this->fuel->config('num_logins_before_lock') -1)
		{
			$this->fuel_users_model->add_error(lang('error_max_attempts', $this->fuel->config('seconds_to_unlock')));
			$user_data['failed_login_timer'] = time();
			$this->fuel->logs->write(lang('auth_log_account_lockout', $this->input->post($field, TRUE), $this->input->ip_address()), 'debug');
		}
		else
		{
			if ($field == 'email')
			{
				$this->fuel_users_model->add_error(lang('error_invalid_email'));
				$this->fuel->logs->write(lang('error_invalid_email', $this->input->post('email', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
			}
			else
			{
				$this->fuel_users_model->add_error(lang('error_invalid_login'));
				$this->fuel->logs->write(lang('auth_log_failed_login', $this->input->post('user_name', TRUE), $this->input->ip_address(), ($user_data['failed_login_attempts'] + 1)), 'debug');
			}
		}
	}