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

use FOF40\Container\Container;
use FOF40\Input\Input;

defined('_JEXEC') || die;

class AtsystemFeatureWafblacklist extends AtsystemFeatureAbstract
{
	protected $loadOrder = 25;

	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		return true;
	}

	/**
	 * Filters visitor access using WAF blacklist rules
	 */
	public function onAfterRoute()
	{
		$db = $this->db;

		$method    = [$db->q(''), $db->q(strtoupper($_SERVER['REQUEST_METHOD']))];
		$option    = [$db->q('')];
		$view      = [$db->q('')];
		$task      = [$db->q('')];

		$fallbackView = version_compare(JVERSION, '3.999.999', 'ge')
			? $this->input->getCmd('controller', '')
			: '';

		$rawView   = $this->input->getCmd('view', $fallbackView);
		$rawTask   = $this->input->getCmd('task', '');
		$rawOption = $this->input->getCmd('option', '');

		if ($rawOption)
		{
			$option[] = $db->q($rawOption);
		}

		if ($rawView)
		{
			$view[] = $db->q($rawView);
		}

		if ($rawTask)
		{
			$task[] = $db->q($rawTask);
		}

		// Parse task=viewName.taskName
		if (empty($rawView) && (strpos($rawTask, '.') !== false))
		{
			[$viewExplode, $taskExplode] = explode('.', $rawTask, 2);
			$view[] = $db->q($viewExplode);
			$task[] = $db->q($taskExplode);
		}
		/**
		 * If we have separate view=viewName and task=taskName variables look for a rule where task=viewName.taskName in
		 * case the user ignored the documentation or tried to be "clever".
		 */
		elseif (strpos($rawTask, '.') === false)
		{
			$task[] = $db->q("$rawView.$rawTask");
		}

		// Let's get the rules for the current input values or the empty ones
		$query = $db->getQuery(true)
			->select('*')
			->from($db->qn('#__admintools_wafblacklists'))
			->where($db->qn('verb') . ' IN(' . implode(',', $method) . ')')
			->where($db->qn('option') . ' IN(' . implode(',', $option) . ')')
			->where($db->qn('view') . ' IN(' . implode(',', $view) . ')')
			->where($db->qn('task') . ' IN(' . implode(',', $task) . ')')
			->where($db->qn('enabled') . ' = ' . $db->q(1))
			->group($db->qn('query'))
			->order($db->qn('query') . ' ASC');

		try
		{
			$rules = $db->setQuery($query)->loadObjectList();
		}
		catch (Exception $e)
		{
			return;
		}

		if (!$rules)
		{
			return;
		}

		// We need FOF 3 loaded for this feature to work
		if (!defined('FOF40_INCLUDED') && !@include_once(JPATH_LIBRARIES . '/fof40/include.php'))
		{
			// This extension requires FOF 4.
			return;
		}

		// Let me see if I am a backend application
		$isBackend = Container::getInstance('com_admintools')->platform->isBackend();

		// I can't use JInput since it will fetch data from cookies, too.
		$inputSources = ['get', 'post'];

		// Ok, let's analyze all the matching rules
		$block = false;

		foreach ($rules as $rule)
		{
			if (!isset($rule->application))
			{
				$rule->application = '';
			}

			// Am I in the correct side of the application?
			// Please note: continue and break have the same meaning inside a switch statement, so we have to
			// continue 2 to break the current switch AND move on the next item of the array
			switch ($rule->application)
			{
				case 'site':
					if ($isBackend)
					{
						continue 2;
					}

					break;

				case 'admin':
					if (!$isBackend)
					{
						continue 2;
					}

					break;
			}

			/**
			 * Make sure the view/task matches.
			 *
			 * This is a bit complicated since we have to take into account that EITHER OF the request AND the rule may
			 * be using the task=viewName.taskName notation. Moreover, empty views and tasks in rules act as wildcards.
			 */
			$view     = $viewExplode ?? $rawView;
			$task     = $taskExplode ?? $rawTask;
			$hasMatch = false;
			// -- Empty view and task: rule applies to entire component
			$hasMatch = $hasMatch || (($rule->view == '') && ($rule->task == ''));
			// -- Request view matches rule view AND rule task is either empty or matches request task
			$hasMatch = $hasMatch || ((!empty($view) && ($rule->view == $view)) && (empty($rule->task) || ($rule->task == $task)));
			// -- Request task matches rule task AND view task is either empty or matches request view
			$hasMatch = $hasMatch || ((!empty($task) && ($rule->task == $task)) && (empty($rule->view) || ($rule->view == $view)));
			// -- Both view and task matched by the rule's task AND the rule's view is empty
			$hasMatch = $hasMatch || ((!empty($task) && !empty($view) && ($rule->task == "$view.$task")) && empty($rule->view));

			if (!$hasMatch)
			{
				continue;
			}

			// Empty query => block everything for this VERB/OPTION/VIEW/TASK combination
			if (!$rule->query)
			{
				$block = true;
				break;
			}

			foreach ($inputSources as $inputSource)
			{
				$inputObject = new Input($inputSource);

				foreach ($inputObject->getData() as $key => $value)
				{
					if ($this->isBlockedByRule($rule, $key, $value))
					{
						$block = true;

						break 3;
					}
				}
			}
		}

		if ($block)
		{
			$extraInfo = '';

			// If the rule matched any variable, let's print the variables that caused the block, so we can inspect later
			if (isset($inputSource) && isset($inputObject))
			{
				// PLEASE NOTE! If POST data is passed, but the GET array is empty, Input will use the whole $_REQUEST
				// array, so $inputSource will be GET even if we truly had a POST request. However this is an edge case
				$extraInfo = "Hash      : " . strtoupper($inputSource) . "\n";
				$extraInfo .= "Variables :\n";
				$extraInfo .= print_r($inputObject->getData(), true);
				$extraInfo .= "\n";
			}

			$this->exceptionsHandler->blockRequest('wafblacklist', null, $extraInfo);
		}
	}

	private function isBlockedByRule($rule, $key, $value, $prefix = '')
	{
		// Handle array values
		if (is_array($value))
		{
			foreach ($value as $subKey => $subValue)
			{
				// Default: assume no prefix was set, in which case the key is the new prefix (array name).
				$newPrefix = $key;

				// If a prefix was set then we have a sub-subkey. The prefix should be prefix[key] instead
				if ($prefix)
				{
					$newPrefix = $prefix . '[' . $key . ']';
				}

				if ($this->isBlockedByRule($rule, $subKey, $subValue, $newPrefix))
				{
					return true;
				}
			}

			return false;
		}

		if ($prefix)
		{
			$key = $prefix . '[' . $key . ']';
		}

		$ruleQuery = $rule->query;

		$found = false;

		// Partial match

		if ($rule->query_type == 'P')
		{
			if (stripos($key, $ruleQuery) !== false)
			{
				$found = true;
			}
		}
		// RegEx match
		elseif ($rule->query_type == 'R')
		{
			$regex  = $ruleQuery;
			$negate = false;

			if (substr($regex, 0, 1) == '!')
			{
				$negate = true;
				$regex  = substr($regex, 1);
			}

			$found = @preg_match($regex, $key) > 0;

			if ($negate)
			{
				$found = !$found;
			}
		}
		// Exact match, empty $ruleQuery
		elseif ($ruleQuery === '')
		{
			$found = true;
		}
		// Exact match, non-empty $ruleQuery
		else
		{
			// Cannot match empty key
			if (empty($key))
			{
				return false;
			}

			if ($key == $ruleQuery)
			{
				$found = true;
			}
		}

		if (!$found)
		{
			return false;
		}

		// Ok, the query parameter is set, do I have any specific rule about the content?
		if ($found)
		{
			// Empty => always block, no matter what
			if (!$rule->query_content)
			{
				return true;
			}

			// I have to run a regex on the value
			$negate = false;
			$regex  = $rule->query_content;

			if (substr($regex, 0, 1) == '!')
			{
				$negate = true;
				$regex  = substr($regex, 1);
			}

			$isFiltered = @preg_match($regex, $value) >= 1;

			if ($negate)
			{
				$isFiltered = !$isFiltered;
			}

			if ($isFiltered)
			{
				return true;
			}
		}

		return false;
	}
}

© 2025 Cubjrnet7