From 1972310fe3f68d917568294adfdbac64b0e06ee2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 07:39:57 +0000 Subject: [PATCH] Login Only block mode: recognise a login however its task is spelled The check for a login attempt compared the task parameter case-sensitively with "user.login" and "login". Joomla lowercases the method before running it, ignores what follows a second dot, and takes the controller from the "controller" parameter if the task has none - so task=user.LOGIN, task=user.login.x or controller=user&task=login logged in from a blocked address in "Login Only" mode. The task is now read like the dispatcher does (requestedTask()), and any login method of com_users or com_login counts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AFJFMMUpch4LGexEiXN8Nc --- src/Extension/Bfstop.php | 23 +++++++++++++---------- tests/Integration/BlockedRequestTest.php | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/Extension/Bfstop.php b/src/Extension/Bfstop.php index 0441db1..0a0d068 100644 --- a/src/Extension/Bfstop.php +++ b/src/Extension/Bfstop.php @@ -748,21 +748,24 @@ private function isPasswordRecoveryRequest() * Detects a request that actually submits login credentials, for * "Login Only" block mode (issue #187): on the frontend, credentials * are submitted to com_users (task=user.login), the backend login form - * posts to com_login (task=login); com_users with task=login is kept - * for backend entry URLs built that way - merely viewing the login - * form (no task, or a display task) doesn't match, so it stays - * reachable. + * posts to com_login (task=login) - merely viewing the login form (no + * task, or a display task) doesn't match, so it stays reachable. + * + * The task is compared as Joomla's dispatcher runs it (see + * requestedTask()): the login method of whatever controller, in any case + * - "task=user.LOGIN", "task=user.login.x" and + * "controller=user&task=login" are all logins. Matching more than + * Joomla would run only blocks a request which wouldn't do anything. */ private function isLoginAttemptRequest() { - $option = $this->requestString('option'); - $task = $this->requestString('task'); - $result = (strcmp($option, 'com_users') == 0 && - (strcmp($task, 'user.login') == 0 || strcmp($task, 'login') == 0)) || - (strcmp($option, 'com_login') == 0 && strcmp($task, 'login') == 0); + $option = strtolower($this->requestString('option')); + list(, $method) = $this->requestedTask(); + $result = ($option === 'com_users' || $option === 'com_login') && $method === 'login'; if ($result) { - $this->logger->log('Detected a login-attempt request (task='.$task.')', Log::DEBUG); + $this->logger->log('Detected a login-attempt request (task='. + $this->requestString('task').')', Log::DEBUG); } return $result; } diff --git a/tests/Integration/BlockedRequestTest.php b/tests/Integration/BlockedRequestTest.php index 36838a2..c67fa3f 100644 --- a/tests/Integration/BlockedRequestTest.php +++ b/tests/Integration/BlockedRequestTest.php @@ -233,6 +233,25 @@ public function testLoginOnlyModeOnlyRejectsLoginAttempts() $this->assertBlocked('option=com_login&task=login'); } + public function testLoginOnlyModeRecognisesEveryWayToLogIn() + { + // Joomla lowercases the method, ignores what follows a second dot and + // takes the controller from its own parameter if the task has no dot: + // each of these runs UserController::login() + $this->configure(array('blockMode' => 'loginonly')); + $this->block(); + $this->assertBlocked('option=com_users&task=user.LOGIN'); + $this->assertBlocked('option=com_users&task=User.Login'); + $this->assertBlocked('option=com_users&task=user.login.x'); + $this->assertBlocked('option=com_users&controller=user&task=login'); + $this->assertBlocked('option=com_users&task=LOGIN'); + $this->assertBlocked('option=com_login&task=LOGIN'); + // still no login + $this->assertNotBlocked('option=com_users&task=user.logout'); + $this->assertNotBlocked('option=com_users&view=login&task=display'); + $this->assertNotBlocked('option=com_content&task=user.login'); + } + public function testPasswordRecoveryStaysReachable() { // a blocked legitimate user must still be able to reset their password