shell bypass 403

Cubjrnet7 Shell


name : BlockedRequestHandler.php
<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2024 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Plugin\System\AdminTools\Utility;

defined('_JEXEC') or die;

use Akeeba\Component\AdminTools\Administrator\Helper\Storage;
use Akeeba\Component\AdminTools\Administrator\Helper\TemplateEmails;
use DateTimeZone;
use Exception;
use Joomla\Application\ApplicationInterface;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\ParameterType;
use Joomla\Registry\Registry;

class BlockedRequestHandler implements DatabaseAwareInterface
{
	use DatabaseAwareTrait;

	/**
	 * Plugin parameters
	 *
	 * @var   Registry
	 * @since 7.0.0
	 */
	protected $pluginParams = null;

	/**
	 * WAF parameters
	 *
	 * @var   Storage
	 * @since 7.0.0
	 */
	protected $wafParams = null;

	/**
	 * Component parameters
	 *
	 * @var   Registry
	 * @since 7.0.0
	 */
	protected $cParams = null;

	private ApplicationInterface $application;

	public function __construct(Registry $pluginParams, Storage $wafParams, Registry $cParams)
	{
		$this->pluginParams = $pluginParams;
		$this->wafParams    = $wafParams;
		$this->cParams      = $cParams;
	}

	/**
	 * @param   string     $templateKey  The template key to send, e.g. 'com_admintools.blockedrequest'
	 * @param   User|null  $user         The user to send the email to. NULL for the currently logged in user.
	 * @param   array      $data         Associative array for tag/variable replacement in the email template.
	 *
	 * @return bool
	 */
	public function sendEmail(string $templateKey, ?User $user = null, array $data = []): bool
	{
		// Do not send emails in the Core version
		if (!defined('ADMINTOOLS_PRO') || !ADMINTOOLS_PRO)
		{
			return true;
		}

		$app  = $this->getApplication();
		$user = $user ?: $app->getIdentity();

		$data = $this->getEmailVariables(
			array_merge(
				[
					'USERNAME' => $user->username,
					'FULLNAME' => $user->name,
				], $data
			)
		);

		try
		{
			TemplateEmails::updateTemplate($templateKey);

			return TemplateEmails::sendMail($templateKey, $data, $user);
		}
		catch (Exception $e)
		{
			return false;
		}
	}

	/**
	 * Logs security exceptions and processes the IP auto-ban for this IP but does NOT block the request.
	 *
	 * This is used when the request needs to be redirected (e.g. admin secret URL parameter), or when we are only
	 * logging potential problems (e.g. failed login).
	 *
	 * @param   string  $reason                    Block reason code
	 * @param   string  $extraLogInformation       Extra information to be written to the text log file
	 * @param   string  $extraLogTableInformation  Extra information to be written to the extradata field of the log
	 *
	 * @return  bool
	 */
	public function logWithoutBlocking($reason, $extraLogInformation = '', $extraLogTableInformation = '')
	{
		if ($this->wafParams->getValue('tsrenable', 0))
		{
			$this->processAutoBan($reason);
		}

		return $this->logBlockedRequest($reason, $extraLogInformation, $extraLogTableInformation);
	}

	/**
	 * Blocks the request, logs it and processes the IP auto-ban.
	 *
	 * This is the full request blocking experience, triggered when we need to immediately abort the request in progress
	 * to prevent a security issue from affecting the application.
	 *
	 * @param   string  $reason                    Block reason code
	 * @param   string  $message                   The message to be shown to the user
	 * @param   string  $extraLogInformation       Extra information to be written to the text log file
	 * @param   string  $extraLogTableInformation  Extra information to be written to the extradata field of the log
	 *                                             table (useful for JSON format)
	 *
	 * @return  void
	 * @throws  Exception
	 */
	public function blockRequest(
		$reason = 'other', $message = '', $extraLogInformation = '', $extraLogTableInformation = ''
	)
	{
		if (empty($message))
		{
			$customMessage = $this->wafParams->getValue('custom403msg', '');
			$message       = trim($customMessage) ?: 'PLG_ADMINTOOLS_MSG_BLOCKED';
		}

		if (!$this->logWithoutBlocking($reason, $extraLogInformation, $extraLogTableInformation))
		{
			return;
		}

		// Merge the default translation with the current translation
		/** @var CMSApplication $app */
		$app = $this->getApplication();

		if ((Text::_('PLG_ADMINTOOLS_MSG_BLOCKED') == 'PLG_ADMINTOOLS_MSG_BLOCKED')
		    && ($message == 'PLG_ADMINTOOLS_MSG_BLOCKED'))
		{
			$message = "Access Denied";
		}
		else
		{
			$message = Text::_($message);
		}

		$message = RescueUrl::processRescueInfoInMessage($message);

		// Show the 403 message
		$use403View = $this->wafParams->getValue('use403view', 0);
		$isFrontend = $app->isClient('site');
		$isApi      = $app->isClient('api');

		if ($isApi)
		{
			@ob_end_clean();

			header('HTTP/1.1 403 Access Denied');

			echo $message;

			$app->close();
		}

		if (!$use403View || !$isFrontend)
		{
			// Using Joomla!'s error page
			$app->input->set('template', null);
			$app->input->set('layout', null);

			throw new Exception($message, 403);
		}

		// Using a view
		$session = $app->getSession();

		if (!$session->get('com_admintools.block', false))
		{
			// This is inside an if-block so that we don't end up in an infinite redirection loop
			$session->set('com_admintools.block', true);
			$session->set('com_admintools.message', $message);

			if ($app->isClient('site') || $app->isClient('administrator'))
			{
				$session->close();
			}

			$app->redirect(Uri::base(), 307);
		}
	}

	/**
	 * Checks if the Rescue URL is being accessed.
	 *
	 * This only applies when IP autoban is enabled and this is an administrator access.
	 *
	 * @return  void
	 */
	public function checkRescueURL()
	{
		$autoban = $this->wafParams->getValue('tsrenable', 0);

		if (!$autoban)
		{
			return;
		}

		// If IP auto-ban is enabled we need to check for a Rescue URL
		RescueUrl::processRescueURL($this);
	}

	public function setApplication(ApplicationInterface $application): void
	{
		$this->application = $application;
	}

	private function getApplication(): ApplicationInterface
	{
		return $this->application;
	}

	/**
	 * Sends a security exception email. Respecting the email throttling settings.
	 *
	 * @param   string     $reason     The blocked request reason
	 * @param   User|null  $recipient  The recipient user. NULL for currently logged in user.
	 * @param   array      $data       Associative array for tag/variable replacement in the email template.
	 *
	 * @return  bool  True on succesfully sent email.
	 */
	private function sendSecurityExceptionEmail(string $reason, ?User $recipient = null, array $data = [])
	{
		if (!$this->isSendingAllowedByEmailThrottling())
		{
			return false;
		}

		return $this->sendEmail(
			'com_admintools.blockedrequest',
			$recipient,
			[
				'REASON' => $reason,
			]
		);
	}

	/**
	 * Get the variables we can use in emails as an associative list (variable => value).
	 *
	 * @param   array  $customVariables  An array of custom variables to add to the return.
	 *
	 * @return  array
	 */
	private function getEmailVariables($customVariables = [])
	{
		$app      = $this->getApplication();
		$siteName = $app->get('sitename');

		$cParams        = ComponentHelper::getParams('com_admintools');
		$email_timezone = $cParams->get('email_timezone', 'AKEEBA/DEFAULT');
		$app            = $this->getApplication();
		$user_tz        = $app->getIdentity()->get('timezone', $app->get('offset', 'GMT'));

		try
		{
			$timezone = new DateTimeZone($user_tz);
		}
		catch (Exception $e)
		{
			$timezone = null;
		}

		if (!empty($email_timezone) && ($email_timezone != 'AKEEBA/DEFAULT'))
		{
			try
			{
				$forcedTimezone = new DateTimeZone($email_timezone);
				$timezone       = $forcedTimezone;
			}
			catch (Exception $e)
			{
				// Just in case someone puts an invalid timezone in there (you can never be too paranoid).
			}
		}

		$date = clone Factory::getDate();
		$date->setTimezone($timezone ?: new DateTimeZone('GMT'));

		$ip = Filter::getIp();

		if ((strpos($ip, '::') === 0) && (strstr($ip, '.') !== false))
		{
			$ip = substr($ip, strrpos($ip, ':') + 1);
		}

		$currentUser = $app->getIdentity();

		if ($currentUser->guest)
		{
			$currentUser = 'Guest';
		}
		else
		{
			$currentUser = $currentUser->username . ' (' . $currentUser->name . ' <' . $currentUser->email . '>)';
		}

		$ipLookupURL = 'https://' . $this->wafParams->getValue('iplookup', 'ip-lookup.net/index.php?ip={ip}');
		$ipLookupURL = str_replace('{ip}', $ip, $ipLookupURL);
		$uri         = Uri::getInstance();
		$url         = $uri->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']);

		return array_merge(
			[
				'USER'     => $currentUser,
				'SITENAME' => $siteName,
				'DATE'     => ($date)->format('Y-m-d H:i:s T', true),
				'IP'       => $ip,
				'URL'      => $url,
				'LOOKUP'   => $ipLookupURL,
				'UA'       => $_SERVER['HTTP_USER_AGENT'],
			], $customVariables
		);
	}

	/**
	 * Logs security exceptions
	 *
	 * @param   string  $reason                    Block reason code
	 * @param   string  $extraLogInformation       Extra information to be written to the text log file
	 * @param   string  $extraLogTableInformation  Extra information to be written to the extradata field of the log
	 *                                             table (useful for JSON format)
	 *
	 * @return bool
	 */
	private function logBlockedRequest($reason, $extraLogInformation = '', $extraLogTableInformation = '')
	{
		$ip = $this->getVisitorIPAddress();

		// No point continuing if I cannot get the visitor's IP address
		if ($ip === false)
		{
			return false;
		}

		// Make sure this IP is not in the Administrator Exclusive Allow IP List
		if ($this->isIPInAdminWhitelist())
		{
			return false;
		}

		// Make sure this IP is not in the Site IP Allow List
		if ($this->isIPInAllowList())
		{
			return false;
		}

		// Make sure this IP is not in the "Do not block these IPs" list
		if ($this->isSafeIP())
		{
			return true;
		}

		// Make sure this IP doesn't resolve to a whitelisted domain
		if ($this->isWhitelistedDomain($ip))
		{
			return true;
		}

		// Get the human-readable blocking reason
		$txtReason = $this->getBlockingReasonHumanReadable($reason, $extraLogTableInformation);

		// Get the email tokens, also used for logging
		$tokens = $this->getEmailVariables(
			[
				'REASON' => $txtReason,
			]
		);

		// Log the security exception to file and the database, if necessary
		$this->logSecurityException($reason, $extraLogInformation, $extraLogTableInformation, $txtReason, $tokens);

		// Email the security exception, if necessary
		$this->emailSecurityException($reason, $tokens);

		return true;
	}

	/**
	 * Checks if an IP address should be automatically banned for raising too many security exceptions over a predefined
	 * time period.
	 *
	 * @param   string  $reason  The reason of the ban
	 *
	 * @return  void
	 */
	private function processAutoBan($reason = 'other')
	{
		// The Core version does not support auto-banning IP addresses
		if (!defined('ADMINTOOLS_PRO') || !ADMINTOOLS_PRO)
		{
			return;
		}

		// We need to be able to get our own IP, right?
		if (!function_exists('inet_pton'))
		{
			return;
		}

		// Get the IP
		$ip = Filter::getIp();

		// No point continuing if we can't get an address, right?
		if (empty($ip) || ($ip == '0.0.0.0'))
		{
			return;
		}

		// Check for repeat offenses
		/** @var DatabaseDriver $db */
		$db           = $this->getDatabase();
		$strikes      = $this->wafParams->getValue('tsrstrikes', 3);
		$numfreq      = $this->wafParams->getValue('tsrnumfreq', 1);
		$frequency    = $this->wafParams->getValue('tsrfrequency', 'hour');
		$mindatestamp = 0;

		switch ($frequency)
		{
			case 'second':
				break;

			case 'minute':
				$numfreq *= 60;
				break;

			case 'hour':
				$numfreq *= 3600;
				break;

			case 'day':
				$numfreq *= 86400;
				break;

			case 'ever':
				$mindatestamp = 946706400; // January 1st, 2000
				break;
		}

		$jNow = clone Factory::getDate();

		if ($mindatestamp == 0)
		{
			$mindatestamp = $jNow->toUnix() - $numfreq;
		}

		$jMinDate = clone Factory::getDate($mindatestamp);
		$minDate  = $jMinDate->toSql();

		$sql = $db->getQuery(true)
			->select('COUNT(*)')
			->from($db->qn('#__admintools_log'))
			->where($db->qn('logdate') . ' >= ' . $db->q($minDate))
			->where($db->qn('ip') . ' = ' . $db->q($ip));
		$db->setQuery($sql);
		try
		{
			$numOffenses = $db->loadResult();
		}
		catch (Exception $e)
		{
			$numOffenses = 0;
		}

		if ($numOffenses < $strikes)
		{
			return;
		}

		// Block the IP
		$myIP = @inet_pton($ip);

		if ($myIP === false)
		{
			return;
		}

		$myIP = inet_ntop($myIP);

		$until     = $jNow->toUnix();
		$numfreq   = $this->wafParams->getValue('tsrbannum', 1);
		$frequency = $this->wafParams->getValue('tsrbanfrequency', 'hour');

		switch ($frequency)
		{
			case 'second':
				$until += $numfreq;
				break;

			case 'minute':
				$numfreq *= 60;
				$until   += $numfreq;
				break;

			case 'hour':
				$numfreq *= 3600;
				$until   += $numfreq;
				break;

			case 'day':
				$numfreq *= 86400;
				$until   += $numfreq;
				break;

			case 'ever':
				$until = 2145938400; // January 1st, 2038 (mind you, UNIX epoch runs out on January 19, 2038!)
				break;
		}

		$jMinDate = clone Factory::getDate($until);
		$minDate  = $jMinDate->toSql();

		$record = (object) [
			'ip'     => $myIP,
			'reason' => $reason,
			'until'  => $minDate,
		];

		// If I'm here it means that we have to ban the user. Let's see if this is a simple autoban or
		// we have to issue a permaban as a result of several attacks
		if ($this->wafParams->getValue('permaban', 0))
		{
			// Ok I have to check the number of autoban
			$query = $db->getQuery(true)
				->select('COUNT(*)')
				->from($db->qn('#__admintools_ipautobanhistory'))
				->where($db->qn('ip') . ' = ' . $db->q($myIP));

			try
			{
				$bans = $db->setQuery($query)->loadResult();
			}
			catch (Exception $e)
			{
				$bans = 0;
			}

			$limit = (int) $this->wafParams->getValue('permabannum', 0);

			if ($limit && ($bans >= $limit))
			{
				$block = (object) [
					'ip'          => $myIP,
					'description' => 'IP automatically blocked after being banned automatically ' . $bans . ' times',
				];

				try
				{
					$db->insertObject('#__admintools_ipblock', $block);
					Cache::resetCache('ipblock');
				}
				catch (Exception $e)
				{
					// This should never happen, however let's prevent a white page if anything goes wrong
				}
			}
		}

		try
		{
			$db->insertObject('#__admintools_ipautoban', $record);
			Cache::resetCache('ipautoban');
		}
		catch (Exception $e)
		{
			// If the IP was already blocked and I have to block it again, I'll have to update the current record
			$db->updateObject('#__admintools_ipautoban', $record, 'ip');
			Cache::resetCache('ipautoban');
		}

		// Send an optional email
		if ($this->wafParams->getValue('emailafteripautoban', ''))
		{
			// Load the component's administrator translation files
			$jlang = $this->getApplication()->getLanguage();
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true);
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true);
			$jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true);

			$substitutions = $this->getEmailVariables(
				[
					'REASON' => $reason,
					'UNTIL'  => $minDate,
				]
			);

			// Send the email
			try
			{
				$recipients = explode(',', $this->wafParams->getValue('emailafteripautoban', ''));
				$recipients = array_map('trim', $recipients);

				foreach ($recipients as $recipient)
				{
					if (empty($recipient))
					{
						continue;
					}

					$recipientUser           = new User();
					$recipientUser->username = $recipient;
					$recipientUser->name     = $recipient;
					$recipientUser->email    = $recipient;
					$data                    = array_merge(RescueUrl::getRescueInformation($recipient), $substitutions);

					$this->sendEmail('com_admintools.ipautoban', $recipientUser, $data);
				}
			}
			catch (Exception $e)
			{
				// Joomla! 3.5 and later throw an exception when crap happens instead of suppressing it and returning false
			}
		}
	}

	/**
	 * Get the visitor IP address. Return false if we cannot get an IP address or if we get 0.0.0.0 (broken IP
	 * forwarding).
	 *
	 * @return  bool|string
	 */
	private function getVisitorIPAddress()
	{
		// Get our IP address
		$ip = Filter::getIp();

		if ((strpos($ip, '::') === 0) && (strstr($ip, '.') !== false))
		{
			$ip = substr($ip, strrpos($ip, ':') + 1);
		}

		// No point continuing if we can't get an address, right?
		if (empty($ip) || ($ip == '0.0.0.0'))
		{
			return false;
		}

		return $ip;
	}

	/**
	 * Is the IP address in the "Never block these IPs" (safe IPs) list?
	 *
	 * @return  bool
	 */
	private function isSafeIP()
	{
		$safeIPs = $this->wafParams->getValue('neverblockips', '') ?: [];

		if (is_string($safeIPs))
		{
			$safeIPs = array_map('trim', explode(',', $safeIPs));
		}

		$safeIPs = array_map(
			function ($x) {
				return is_array($x) ? $x[0] : $x;
			}, is_array($safeIPs) ? $safeIPs : []
		);

		return !empty($safeIPs) && Filter::IPinList($safeIPs) ? true : false;
	}

	/**
	 * Is the IP address in the Administrator IP Whitelist?
	 *
	 * @return  bool
	 */
	private function isIPInAdminWhitelist(): bool
	{
		if ($this->wafParams->getValue('ipwl', 0) != 1)
		{
			return false;
		}

		$ipTable = Cache::getCache('adminiplist');

		if (!empty($ipTable) && Filter::IPinList($ipTable))
		{
			return true;
		}

		return false;
	}

	/**
	 * Is the IP address in the Site IP Allow List?
	 *
	 * @return  bool
	 *
	 * @since   7.2.4
	 */
	private function isIPInAllowList(): bool
	{
		$ipTable = Cache::getCache('ipallow');

		if (!empty($ipTable) && Filter::IPinList($ipTable))
		{
			return true;
		}

		return false;
	}

	/**
	 * Does the IP address resolve to one of the whitelisted domain names?
	 *
	 * @param   string  $ip
	 *
	 * @return  bool
	 */
	private function isWhitelistedDomain($ip)
	{
		static $whitelistDomains = null;

		if (is_null($whitelistDomains))
		{
			$whitelistDomains = $this->wafParams->getValue('whitelist_domains', []);

			if (is_string($whitelistDomains))
			{
				$whitelistDomains = array_map('trim', explode(',', $whitelistDomains));
			}

			$whitelistDomains = array_map(function($x) {
				return is_array($x) ? $x[0] : $x;
			}, is_array($whitelistDomains) ? $whitelistDomains : []);
		}

		if (!empty($whitelistDomains))
		{
			$remote_domain = @gethostbyaddr($ip);

			if (empty($remote_domain))
			{
				return false;
			}

			foreach ($whitelistDomains as $domain)
			{
				$domain = trim($domain);

				if (strrpos($remote_domain, $domain) === strlen($remote_domain) - strlen($domain))
				{
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Get the blocking reason in a human readable format
	 *
	 * @param   string  $reason
	 * @param   string  $extraLogTableInformation
	 *
	 * @return  string
	 */
	private function getBlockingReasonHumanReadable($reason, $extraLogTableInformation)
	{
		// Load the component's administrator translation files
		$jlang = $this->getApplication()->getLanguage();
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true);
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true);
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true);

		// Get the reason in human readable format
		$txtReason = Text::_('COM_ADMINTOOLS_LOG_LBL_REASON_' . strtoupper($reason));

		if (empty($extraLogTableInformation))
		{
			return $txtReason;
		}

		// Get extra information
		[$logReason,] = explode('|', $extraLogTableInformation);

		return $txtReason . " ($logReason)";
	}

	/**
	 * Write a security exception to the log, as long as logging is enabled and the $reason is not one of the
	 * $reasons_nolog ones
	 *
	 * @param   string  $reason
	 * @param   string  $extraLogInformation
	 * @param   string  $extraLogTableInformation
	 * @param   string  $txtReason
	 * @param   array   $tokens
	 *
	 * @return  void
	 */
	private function logSecurityException($reason, $extraLogInformation, $extraLogTableInformation, $txtReason, $tokens)
	{
		$reasonsNoLog = $this->wafParams->getValue('reasons_nolog', []);

		// Handle legacy data
		if (is_string($reasonsNoLog))
		{
			$reasonsNoLog = explode(',', $reasonsNoLog);
		}

		$reasonsNoLog = is_array($reasonsNoLog) ? $reasonsNoLog : [];

		// If it's a no-log reason let's get outta here
		if (!$this->wafParams->getValue('logbreaches', 0) || in_array($reason, $reasonsNoLog))
		{
			return;
		}

		// Log to file
		$this->logSecurityExceptionToFile($reason, $extraLogInformation, $txtReason, $tokens);

		// Log to the database table
		$this->logSecurityExceptionToDatabase($reason, $extraLogTableInformation, $tokens);
	}

	/**
	 * Log a security exception to our log file
	 *
	 * @param   string  $reason
	 * @param   string  $extraLogInformation
	 * @param   string  $txtReason
	 * @param   array   $tokens
	 */
	private function logSecurityExceptionToFile($reason, $extraLogInformation, $txtReason, $tokens)
	{
		// Write to the log file only if we're told to
		if (!$this->wafParams->getValue('logfile', 0))
		{
			return;
		}

		// Get the log filename
		$logpath = $this->getApplication()->get('log_path');
		$fname   = $logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.php';

		// -- Check the file size. If it's over 1Mb, archive and start a new log.
		if (@file_exists($fname))
		{
			$fsize = filesize($fname);

			if ($fsize > 1048756)
			{
				$altFile = substr($fname, 0, -4) . '.1.php';

				if (@file_exists($altFile))
				{
					unlink($altFile);
				}

				@copy($fname, $altFile);
				@unlink($fname);
			}
		}

		// If the main log file does not exist yet create a new one.
		if (!file_exists($fname))
		{
			$content = <<< END
php
/**
 * =====================================================================================================================
 * Admin Tools debug log file
 * =====================================================================================================================
 *
 * This file contains a dump of the requests which were blocked by Admin Tools. By definition, this file does contain
 * a lot of "hacking signatures" since this is what the Admin Tools component is designed to stop and this is the file
 * logging all these hacking attempts.
 *
 * You can disable the creation of this file by going to Components, Admin Tools, Web Application Firewall, Configure
 * WAF and setting the "Keep a debug log file" option to NO. This is the recommended setting. You should only set this
 * option to YES if you are troubleshooting an issue (Admin Tools is blocking access to your site).
 *
 * Some hosts will mistakenly report this file as suspicious or hacked. As a result they might issue an automated
 * warning and / or block access to your site. Should that happen please ask your host to look in this file and read
 * this header. This file is SAFE since the only executable statement is die() below which prevents the file from being
 * executed at all. If your host does not understand that this file is safe or does not know how to add an exception in
 * their automated scanner to exempt Joomla's log files (all files under this directory) from being flagged as hacked /
 * suspicious we strongly recommend going to a different host that understands how PHP works. It will be safer for you
 * as well. 
 */
 
die();
END;
			$content = "?$content?";
			$content .= ">\n\n";
			file_put_contents($fname, '<' . $content);
		}

		// -- Log the exception
		$fp = @fopen($fname, 'a');

		if ($fp === false)
		{
			return;
		}

		fwrite($fp, str_repeat('-', 79) . PHP_EOL);
		fwrite($fp, "Blocking reason: " . $reason . PHP_EOL . str_repeat('-', 79) . PHP_EOL);
		fwrite($fp, "Reason     : " . $txtReason . PHP_EOL);
		fwrite($fp, 'Timestamp  : ' . gmdate('Y-m-d H:i:s') . " GMT" . PHP_EOL);
		fwrite($fp, 'Local time : ' . $tokens['[DATE]'] . " " . PHP_EOL);
		fwrite($fp, 'URL        : ' . $tokens['[URL]'] . PHP_EOL);
		fwrite($fp, 'User       : ' . $tokens['[USER]'] . PHP_EOL);
		fwrite($fp, 'IP         : ' . $tokens['[IP]'] . PHP_EOL);
		fwrite($fp, 'UA         : ' . $tokens['[UA]'] . PHP_EOL);

		if (!empty($extraLogInformation))
		{
			fwrite($fp, $extraLogInformation . PHP_EOL);
		}

		fwrite($fp, PHP_EOL . PHP_EOL);
		fclose($fp);
	}

	/**
	 * Log a security exception to the database table
	 *
	 * @param   string  $reason
	 * @param   string  $extraLogInformation
	 * @param   array   $tokens
	 *
	 *
	 * @since version
	 */
	private function logSecurityExceptionToDatabase($reason, $extraLogTableInformation, $tokens)
	{
		try
		{
			/** @var DatabaseDriver $db */
			$db   = $this->getDatabase();
			$date = clone Factory::getDate();
			$url  = $tokens['URL'];

			if (strlen($url) > 10240)
			{
				$url = substr($url, 0, 10240);
			}

			$logEntry = (object) [
				'logdate'   => $date->toSql(),
				'ip'        => $tokens['IP'],
				'url'       => $url,
				'reason'    => $reason,
				'extradata' => $extraLogTableInformation,
			];

			$db->insertObject('#__admintools_log', $logEntry);
		}
		catch (Exception $e)
		{
			// Do nothing if the query fails
		}
	}

	/**
	 * Sends information about the security exception by email
	 *
	 * @param   string  $reason
	 * @param   array   $tokens
	 *
	 * @return  bool
	 */
	private function emailSecurityException($reason, $tokens)
	{
		$emailOnException = $this->wafParams->getValue('emailbreaches', '');
		$reasonsNoEmail   = $this->wafParams->getValue('reasons_noemail', '') ?: [];
		$reasonsNoEmail   = is_string($reasonsNoEmail) ? explode(',', $reasonsNoEmail) : $reasonsNoEmail;

		if (empty($emailOnException) || in_array($reason, $reasonsNoEmail))
		{
			return true;
		}

		// Send the email
		try
		{
			$recipients = explode(',', $emailOnException);
			$recipients = array_map('trim', $recipients);

			foreach ($recipients as $recipient)
			{
				if (empty($recipient))
				{
					continue;
				}

				if (empty($recipient))
				{
					continue;
				}

				$recipientUser           = new User();
				$recipientUser->username = $recipient;
				$recipientUser->name     = $recipient;
				$recipientUser->email    = $recipient;
				$tokens                  = array_merge($tokens, RescueUrl::getRescueInformation($recipient));

				$this->sendSecurityExceptionEmail($reason, $recipientUser, $tokens);
			}
		}
		catch (Exception $e)
		{
		}

		return true;
	}

	/**
	 * Is sending an email allowed by the email throttling feature?
	 *
	 * @return  bool
	 *
	 * @since   7.2.2
	 */
	private function isSendingAllowedByEmailThrottling(): bool
	{
		$cParams = ComponentHelper::getParams('com_admintools');

		//  If the throttling feature is disabled allow sending the email.
		if ($cParams->get('email_throttle', 1) != 1)
		{
			return true;
		}

		// Get the frequency limit options
		$maxAllowedEmails = $cParams->get('email_num', 5);
		$timePeriod       = $cParams->get('email_numfreq', 15);
		$timeUOM          = $cParams->get('email_freq', 'minutes');

		switch ($timeUOM)
		{
			case 'seconds':
				$earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'S'));
				break;

			case 'minutes':
				$earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'M'));
				break;

			case 'hours':
				$earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'H'));
				break;

			case 'days':
				$earliestDate = Factory::getDate()->sub(new \DateInterval('P' . $timePeriod . 'D'));
				break;

			case 'ever':
			default:
				$earliestDate = Factory::getDate('2000-01-01 00:00:00');
				break;
		}

		$reasonsNoLog = $this->wafParams->getValue('reasons_nolog', []) ?: [];
		$reasonsNoLog = is_array($reasonsNoLog)
			? $reasonsNoLog
			: array_map('trim', @explode(',', $reasonsNoLog));

		/** @var DatabaseDriver $db */
		$db      = $this->getDatabase();
		$logDate = $earliestDate->toSql();
		$sql     = $db->getQuery(true)
			->select('COUNT(*)')
			->from($db->qn('#__admintools_log'))
			->where($db->qn('logdate') . ' >= :logDate')
			->bind(':logDate', $logDate);

		// Apply the where clause only if we have excluded any reason from logging
		if (!empty($reasonsNoLog))
		{
			$sql->whereNotIn($db->qn('reason'), $reasonsNoLog, ParameterType::STRING);
		}

		$db->setQuery($sql);

		try
		{
			$numOffenses = $db->loadResult() ?: 0;
		}
		catch (Exception $e)
		{
			$numOffenses = 0;
		}

		return $numOffenses <= $maxAllowedEmails;
	}
}

© 2025 Cubjrnet7