From 313f100789ced8fe21a8aa7b589f039a8d32ae75 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:00:00 +0100 Subject: [PATCH 1/4] Allow NULL user_id in login history --- lib/Users.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/Users.php b/lib/Users.php index e4b75e615..873c361b3 100755 --- a/lib/Users.php +++ b/lib/Users.php @@ -956,6 +956,20 @@ public function usernameExists($username) public function addLoginHistory($userID, $siteID, $ip, $userAgent, $wasSuccessful) { + if ($userID === null || $userID === '') + { + $userIDSQL = 'NULL'; + } + else + { + $userIDSQL = $this->_db->makeQueryInteger($userID); + } + + if ($siteID === null || $siteID === '') + { + $siteID = 0; + } + if (ENABLE_HOSTNAME_LOOKUP) { $hostname = @gethostbyaddr($ip); @@ -986,8 +1000,8 @@ public function addLoginHistory($userID, $siteID, $ip, $userAgent, %s, NOW() )", - $userID, - $siteID, + $userIDSQL, + $this->_db->makeQueryInteger($siteID), $this->_db->makeQueryString($ip), $this->_db->makeQueryString($userAgent), $this->_db->makeQueryString($hostname), From 79320521d80d13a0fc73c9fe2a435efdcc37f9d3 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:00:00 +0100 Subject: [PATCH 2/4] Log failed login attempts for unknown usernames --- lib/Session.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/Session.php b/lib/Session.php index b56c89564..8312d6ddc 100755 --- a/lib/Session.php +++ b/lib/Session.php @@ -676,6 +676,36 @@ public function processLogin($username, $password, $addToHistory = true) $this->_isLoggedIn = false; $this->_loginError = 'Invalid username or password.'; + if (isset($_SERVER['REMOTE_ADDR'])) + { + $ip = $_SERVER['REMOTE_ADDR']; + } + else + { + $ip = ''; + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + $userAgent = $_SERVER['HTTP_USER_AGENT']; + } + else + { + $userAgent = ''; + } + + /* Log the login as unsuccessful. */ + if ($addToHistory) + { + $users->addLoginHistory( + null, + 0, + $ip, + $userAgent, + false + ); + } + return; } From 8c400d1acca6154a3b5be76ba815a99b95711c89 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:00:00 +0100 Subject: [PATCH 3/4] Block login attempts after 10 failed attempts from the same IP within 10 minutes --- lib/Session.php | 80 +++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/lib/Session.php b/lib/Session.php index 8312d6ddc..ed8616646 100755 --- a/lib/Session.php +++ b/lib/Session.php @@ -667,6 +667,50 @@ public function processLogin($username, $password, $addToHistory = true) { $db = DatabaseConnection::getInstance(); + if (isset($_SERVER['REMOTE_ADDR'])) + { + $ip = $_SERVER['REMOTE_ADDR']; + } + else + { + $ip = ''; + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + $userAgent = $_SERVER['HTTP_USER_AGENT']; + } + else + { + $userAgent = ''; + } + + if (!empty($ip)) + { + $sql = sprintf( + "SELECT + COUNT(*) AS failedAttempts + FROM + user_login + WHERE + ip = %s + AND + successful = 0 + AND + date >= (NOW() - INTERVAL 10 MINUTE)", + $db->makeQueryString($ip) + ); + + $rs = $db->getAssoc($sql); + if (!empty($rs) && $rs['failedAttempts'] > 10) + { + $this->_isLoggedIn = false; + $this->_loginError = 'Too many failed login attempts. Please try again later.'; + + return; + } + } + /* Is the login information supplied correct? Get the status flag. */ $users = new Users(-1); $loginStatus = $users->isCorrectLogin($username, $password); @@ -676,24 +720,6 @@ public function processLogin($username, $password, $addToHistory = true) $this->_isLoggedIn = false; $this->_loginError = 'Invalid username or password.'; - if (isset($_SERVER['REMOTE_ADDR'])) - { - $ip = $_SERVER['REMOTE_ADDR']; - } - else - { - $ip = ''; - } - - if (isset($_SERVER['HTTP_USER_AGENT'])) - { - $userAgent = $_SERVER['HTTP_USER_AGENT']; - } - else - { - $userAgent = ''; - } - /* Log the login as unsuccessful. */ if ($addToHistory) { @@ -758,24 +784,6 @@ public function processLogin($username, $password, $addToHistory = true) return; } - if (isset($_SERVER['REMOTE_ADDR'])) - { - $ip = $_SERVER['REMOTE_ADDR']; - } - else - { - $ip = ''; - } - - if (isset($_SERVER['HTTP_USER_AGENT'])) - { - $userAgent = $_SERVER['HTTP_USER_AGENT']; - } - else - { - $userAgent = ''; - } - switch ($loginStatus) { case LOGIN_INVALID_PASSWORD: From 4c2f58802a71c34f7042131d19799c2f19c34ad3 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:49:56 +0100 Subject: [PATCH 4/4] Truncate user_login for tagged scenarios --- .../GET_POST_requestsSecurity.feature | 22 ++++++------ test/features/bootstrap/SecurityContext.php | 34 +++++++++++++++++++ test/features/moduleMainPagesSecurity.feature | 20 +++++------ test/features/moduleSubPagesSecurity.feature | 12 +++---- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/test/features/GET_POST_requestsSecurity.feature b/test/features/GET_POST_requestsSecurity.feature index ef1b56080..e53c77756 100644 --- a/test/features/GET_POST_requestsSecurity.feature +++ b/test/features/GET_POST_requestsSecurity.feature @@ -3,7 +3,7 @@ Feature: Security using ACL - actions - GET & POST In order to protect sensitive information from users who shouldd not have access to them All accesses in the system need to be controlled by the Access Control List -@candidates @actions +@candidates @actions @reset_login_attempts Scenario Outline: Candidate module actions Given I am logged in with access level @@ -223,7 +223,7 @@ Scenario Outline: Candidate module actions -@joborders @actions +@joborders @actions @reset_login_attempts Scenario Outline: Job Order module actions Given I am logged in with access level @@ -411,7 +411,7 @@ Scenario Outline: Job Order module actions - @companies @actions + @companies @actions @reset_login_attempts Scenario Outline: Companies module actions Given I am logged in with access level @@ -526,7 +526,7 @@ Examples: | ROOT | POST | index.php?m=companies&a=createAttachment | | -@contacts @actions +@contacts @actions @reset_login_attempts Scenario Outline: Contacts module actions Given I am logged in with access level @@ -640,7 +640,7 @@ Scenario Outline: Contacts module actions | ROOT | POST | index.php?m=contacts&a=edit | | | ROOT | POST | index.php?m=contacts&a=addActivityScheduleEvent | | -@activities @actions +@activities @actions @reset_login_attempts Scenario Outline: Activity module actions Given I am logged in with access level @@ -674,7 +674,7 @@ Scenario Outline: Activity module actions | ROOT | GET | index.php?m=activity&a=listByViewDataGrid | | | ROOT | POST | index.php?m=activity&a=viewByDate | | -@dashboard @home @actions +@dashboard @home @actions @reset_login_attempts Scenario Outline: Home module actions Given I am logged in with access level @@ -724,7 +724,7 @@ Scenario Outline: Home module actions | ROOT | GET | index.php?m=home&a=getAttachment | | | ROOT | GET | index.php?m=home&a=home | | -@lists @actions +@lists @actions @reset_login_attempts Scenario Outline: Lists module actions Given I am logged in with access level @@ -783,7 +783,7 @@ Scenario Outline: Lists module actions | ROOT | GET | index.php?m=lists&a=listByView | | -@calendar @actions +@calendar @actions @reset_login_attempts Scenario Outline: Calendar module actions Given I am logged in with access level @@ -833,7 +833,7 @@ Scenario Outline: Calendar module actions | ROOT | POST | index.php?m=calendar&a=addEvent | | | ROOT | POST | index.php?m=calendar&a=editEvent | | -@reports @actions +@reports @actions @reset_login_attempts Scenario Outline: Reports module actions Given I am logged in with access level @@ -907,7 +907,7 @@ Scenario Outline: Reports module actions | ROOT | GET | index.php?m=reports&a=generateEEOReportPreview | | | ROOT | GET | index.php?m=reports&a=reports | | - @settings @actions + @settings @actions @reset_login_attempts Scenario Outline: Settings module actions Given I am logged in with access level @@ -1369,4 +1369,4 @@ Scenario Outline: Reports module actions #When I do GET request "index.php?m=settings&a=ajax_wizardWebsite" #And the response should contain "You don't have permission" - \ No newline at end of file + diff --git a/test/features/bootstrap/SecurityContext.php b/test/features/bootstrap/SecurityContext.php index f4f85ce09..1c83b6397 100644 --- a/test/features/bootstrap/SecurityContext.php +++ b/test/features/bootstrap/SecurityContext.php @@ -33,6 +33,40 @@ public function __construct() { } + /** + * Test-only: Some security scenarios intentionally perform many failed login attempts (e.g. disabled users). + * This can accumulate rows in `user_login` and trigger the IP-based brute-force lockout, which then breaks + * subsequent login steps within the same CI run. For scenarios tagged with @reset_login_attempts, reset the + * login attempt history to keep tests isolated from this side effect. + * + * @BeforeScenario + */ + public function resetLoginAttemptsForScenario($event) + { + $scenario = $event->getScenario(); + if (method_exists($scenario, 'hasTag')) + { + $hasTag = $scenario->hasTag('reset_login_attempts'); + } + else + { + $tags = $scenario->getTags(); + $hasTag = in_array('reset_login_attempts', $tags, true); + } + + if (!$hasTag) + { + return; + } + + $db = DatabaseConnection::getInstance(); + $result = $db->query('TRUNCATE TABLE user_login'); + if ($result === false) + { + throw new Exception('Failed to truncate user_login for scenario: ' . $scenario->getTitle()); + } + } + /** * @Given I am logged in with :accessLevel access level */ diff --git a/test/features/moduleMainPagesSecurity.feature b/test/features/moduleMainPagesSecurity.feature index 825607d70..ffa31cc2d 100644 --- a/test/features/moduleMainPagesSecurity.feature +++ b/test/features/moduleMainPagesSecurity.feature @@ -5,7 +5,7 @@ Feature: Access Level to objects check - main pages ######## DASHBOARD(HOME) ####### - @javascript @dashboard + @javascript @dashboard @reset_login_attempts Scenario Outline: Dashboard module visibility Given I am logged in with access level And I am on "/index.php?m=home" @@ -32,7 +32,7 @@ Feature: Access Level to objects check - main pages ####### ACTIVITIES ####### - @javascript @activities + @javascript @activities @reset_login_attempts Scenario Outline: Activities module visibility Given I am logged in with access level And I am on "/index.php?m=activity" @@ -64,7 +64,7 @@ Feature: Access Level to objects check - main pages ####### JOB ORDERS ####### - @javascript @joborders + @javascript @joborders @reset_login_attempts Scenario Outline: Job Orders module visibility Given I am logged in with access level And I am on "/index.php?m=joborders" @@ -98,7 +98,7 @@ Feature: Access Level to objects check - main pages ####### CANDIDATES ####### - @javascript @candidates + @javascript @candidates @reset_login_attempts Scenario Outline: Candidates module visibility Given I am logged in with access level And I am on "/index.php?m=candidates" @@ -134,7 +134,7 @@ Feature: Access Level to objects check - main pages ####### COMPANIES ####### - @javascript @companies + @javascript @companies @reset_login_attempts Scenario Outline: Companies module visibility Given I am logged in with access level And I am on "/index.php?m=companies" @@ -167,7 +167,7 @@ Feature: Access Level to objects check - main pages ####### CONTACTS ####### - @javascript @contacts + @javascript @contacts @reset_login_attempts Scenario Outline: Contacts module visibility Given I am logged in with access level And I am on "/index.php?m=contacts" @@ -201,7 +201,7 @@ Feature: Access Level to objects check - main pages ####### LISTS ####### - @javascript @lists + @javascript @lists @reset_login_attempts Scenario Outline: Lists module visibility Given I am logged in with access level And I am on "/index.php?m=lists" @@ -227,7 +227,7 @@ Feature: Access Level to objects check - main pages ####### REPORTS ####### - @javascript @reports + @javascript @reports @reset_login_attempts Scenario Outline: Reports module visibility Given I am logged in with access level And I am on "/index.php?m=reports" @@ -257,7 +257,7 @@ Feature: Access Level to objects check - main pages ####### SETTINGS ####### -@javascript @settings +@javascript @settings @reset_login_attempts Scenario Outline: Settings module visibility Given I am logged in with access level And I am on "/index.php?m=settings" @@ -283,7 +283,7 @@ Feature: Access Level to objects check - main pages ####### CALENDAR ####### - @javascript @calendar + @javascript @calendar @reset_login_attempts Scenario Outline: Calendar module visibility Given I am logged in with access level And I am on "/index.php?m=calendar" diff --git a/test/features/moduleSubPagesSecurity.feature b/test/features/moduleSubPagesSecurity.feature index f0e4c9645..c5f6e424b 100644 --- a/test/features/moduleSubPagesSecurity.feature +++ b/test/features/moduleSubPagesSecurity.feature @@ -11,7 +11,7 @@ Feature: Access Level to objects check - sub pages (show, ...) ####### JOB ORDERS ####### - @javascript @joborders + @javascript @joborders @reset_login_attempts Scenario Outline: Job Order Show page visibility Given I am logged in with access level And I am on "/index.php?m=joborders" @@ -50,7 +50,7 @@ Feature: Access Level to objects check - sub pages (show, ...) ####### CANDIDATES ####### - @javascript @candidates + @javascript @candidates @reset_login_attempts Scenario Outline: Candidate Show page visibility Given I am logged in with access level And I am on "/index.php?m=candidates" @@ -93,7 +93,7 @@ Feature: Access Level to objects check - sub pages (show, ...) ####### COMPANIES ####### - @javascript @companies + @javascript @companies @reset_login_attempts Scenario Outline: Company Show page visibility for disabled level Given I am logged in with access level And I am on "/index.php?m=home" @@ -153,7 +153,7 @@ Feature: Access Level to objects check - sub pages (show, ...) ####### CONTACTS ####### - @javascript @contacts + @javascript @contacts @reset_login_attempts Scenario Outline: Contacts Show page visibility Given I am logged in with access level And I am on "/index.php?m=contacts" @@ -187,7 +187,7 @@ Feature: Access Level to objects check - sub pages (show, ...) ####### LISTS ####### - @javascript @lists + @javascript @lists @reset_login_attempts Scenario Outline: Lists Show page visibility Given I am logged in with access level And I am on "/index.php?m=lists" @@ -229,4 +229,4 @@ Feature: Access Level to objects check - sub pages (show, ...) # no sub pages ##missing checks for quick action menus on Show pages - \ No newline at end of file +