From f21bcdd6bb4d1b5ba31f8bb3a2bcdb43c7813946 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 09:59:49 -0500 Subject: [PATCH] feat: Web Application Firewall with 10 security shields (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shields implemented: - SQLiShield — SQL injection detection on GET/POST/COOKIE - XSSShield — cross-site scripting detection on GET/POST - MUAShield — malicious user agent blocking (configurable list) - RFIShield — remote file inclusion prevention - DFIShield — directory traversal / local file inclusion prevention - Block sensitive files (htaccess.txt, configuration.php-dist, etc.) - Block direct PHP execution in images/media/tmp/cache/logs - Block template switching (tmpl=/template= params) - IP deny list with CIDR/wildcard support - Admin secret URL parameter with session persistence All shields individually toggleable. Master users and trusted IPs bypass. Blocked requests logged to #__mokowaas_waf_log table. Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../admin/src/Model/DashboardModel.php | 2 +- .../en-GB/plg_system_mokowaas_firewall.ini | 55 +- .../mokowaas_firewall.xml | 143 ++++- .../sql/install.mysql.sql | 13 + .../sql/uninstall.mysql.sql | 1 + .../src/Extension/Firewall.php | 524 +++++++++++++++--- 6 files changed, 648 insertions(+), 90 deletions(-) create mode 100644 src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql create mode 100644 src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql diff --git a/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php b/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php index 5fc3091c..0fad9b2e 100644 --- a/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php +++ b/src/packages/com_mokowaas/admin/src/Model/DashboardModel.php @@ -32,7 +32,7 @@ class DashboardModel extends BaseDatabaseModel 'icon' => 'icon-lock', 'category' => 'security', 'label' => 'Firewall', - 'description' => 'HTTPS enforcement, trusted IPs, session timeout, upload restrictions, and password policy.', + 'description' => 'Web Application Firewall — SQLi, XSS, RFI, DFI shields, IP blocklist, admin secret URL, file protection.', 'protected' => false, ], 'mokowaas_tenant' => [ diff --git a/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini index ea604380..2d544bc2 100644 --- a/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini +++ b/src/packages/plg_system_mokowaas_firewall/language/en-GB/plg_system_mokowaas_firewall.ini @@ -3,28 +3,65 @@ ; License: GPL-3.0-or-later PLG_SYSTEM_MOKOWAAS_FIREWALL="System - MokoWaaS Firewall" -PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC="HTTPS enforcement, trusted IPs, session timeout, upload restrictions, and password policy." +PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC="Web Application Firewall with security shields, IP management, request inspection, and access control." PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC="Network & Session" PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC_DESC="HTTPS, session timeout, and trusted IP settings." PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_LABEL="Force HTTPS" -PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS. Recommended for production sites." +PLG_SYSTEM_MOKOWAAS_FIREWALL_FORCE_HTTPS_DESC="Redirect all HTTP requests to HTTPS." PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_LABEL="Admin Session Timeout (minutes)" -PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_DESC="Idle timeout in minutes for admin sessions. 0 = use Joomla default. Master users and trusted IPs are exempt." +PLG_SYSTEM_MOKOWAAS_FIREWALL_SESSION_TIMEOUT_DESC="Idle timeout for admin sessions. 0 = Joomla default. Master users and trusted IPs exempt." PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_LABEL="Trusted IPs" -PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_DESC="IP addresses or CIDR blocks that bypass session timeout. Supports exact IPs, CIDR (10.0.0.0/8), and wildcards (192.168.1.*)." +PLG_SYSTEM_MOKOWAAS_FIREWALL_TRUSTED_IPS_DESC="IPs that bypass session timeout and WAF shields. Supports exact, CIDR, and wildcard." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF="Web Application Firewall" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF_DESC="Threat detection shields that inspect incoming requests." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_LABEL="Enable WAF" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_DESC="Master toggle for all WAF shields." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_LABEL="SQLiShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_DESC="Block SQL injection patterns in GET, POST, and COOKIE data." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_LABEL="XSSShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_DESC="Block cross-site scripting patterns in GET and POST data." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LABEL="MUAShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_DESC="Block known malicious user agents (scanners, bots, attack tools)." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_LABEL="User Agent Blocklist" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_DESC="Comma-separated user agent fragments to block." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_LABEL="RFIShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_DESC="Block remote file inclusion attempts (URLs in GET parameters)." +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_LABEL="DFIShield" +PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_DESC="Block directory traversal and local file inclusion attempts." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS="Access Control" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS_DESC="IP blocking, admin secret URL, and login restrictions." +PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_LABEL="IP Deny List" +PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_DESC="Block specific IPs or CIDR ranges. Checked before all other shields." +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_LABEL="Admin Secret URL Parameter" +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_DESC="Require ?secret=VALUE to access /administrator. Leave empty to disable." +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_LABEL="Secret Failure Redirect" +PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_DESC="URL to redirect when admin secret is missing. Empty = 403 Forbidden." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_LABEL="Forbid Frontend Super User Login" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_DESC="Prevent Super User accounts from logging in on the frontend." + +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION="File & Template Protection" +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION_DESC="Block access to sensitive files and prevent template switching." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_LABEL="Block Sensitive Files" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_DESC="Block access to htaccess.txt, configuration.php-dist, and similar files." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_LABEL="Block Direct PHP Access" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_DESC="Block PHP execution in images/, media/, tmp/, cache/, logs/ directories." +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_LABEL="Block Template Switching" +PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_DESC="Block tmpl= and template= URL parameters (tmpl=component allowed)." PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD="Password Policy" -PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC="Minimum password complexity requirements for all users." +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC="Minimum password complexity requirements." PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_LABEL="Minimum Password Length" -PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_DESC="Minimum number of characters required." +PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_LENGTH_DESC="Minimum characters required." PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_UPPER_LABEL="Require Uppercase" PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_NUMBER_LABEL="Require Number" PLG_SYSTEM_MOKOWAAS_FIREWALL_PASSWORD_SPECIAL_LABEL="Require Special Character" PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS="Upload Restrictions" -PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC="Override Joomla's upload settings at runtime." +PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC="Override Joomla upload settings at runtime." PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_LABEL="Allowed File Types" -PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_DESC="Comma-separated list of permitted file extensions." +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_TYPES_DESC="Comma-separated permitted file extensions." PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_LABEL="Max Upload Size (MB)" -PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_DESC="Maximum file upload size in megabytes." +PLG_SYSTEM_MOKOWAAS_FIREWALL_UPLOAD_SIZE_DESC="Maximum upload size in megabytes." diff --git a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml index 0acba14d..0dd340b1 100644 --- a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml +++ b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml @@ -8,16 +8,24 @@ GPL-3.0-or-later hello@mokoconsulting.tech https://mokoconsulting.tech - 02.32.09 + 02.32.00 PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC Moko\Plugin\System\MokoWaaSFirewall src + sql services language + + sql/install.mysql.sql + + + sql/uninstall.mysql.sql + + en-GB/plg_system_mokowaas_firewall.ini en-GB/plg_system_mokowaas_firewall.sys.ini @@ -25,6 +33,7 @@ +
@@ -52,6 +61,137 @@ buttons="add,remove,move" />
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + +
+ +
@@ -82,6 +222,7 @@
+
diff --git a/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql b/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql new file mode 100644 index 00000000..3bdc972f --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/sql/install.mysql.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS `#__mokowaas_waf_log` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `ip` VARCHAR(45) NOT NULL, + `uri` VARCHAR(2048) NOT NULL DEFAULT '', + `rule` VARCHAR(50) NOT NULL, + `detail` VARCHAR(512) NOT NULL DEFAULT '', + `user_agent` VARCHAR(512) NOT NULL DEFAULT '', + `created` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `idx_ip` (`ip`), + KEY `idx_rule` (`rule`), + KEY `idx_created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql b/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql new file mode 100644 index 00000000..0d230534 --- /dev/null +++ b/src/packages/plg_system_mokowaas_firewall/sql/uninstall.mysql.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `#__mokowaas_waf_log`; diff --git a/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php b/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php index 60ea056b..c2b096b3 100644 --- a/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php +++ b/src/packages/plg_system_mokowaas_firewall/src/Extension/Firewall.php @@ -11,6 +11,7 @@ namespace Moko\Plugin\System\MokoWaaSFirewall\Extension; defined('_JEXEC') or die; use Joomla\CMS\Factory; +use Joomla\CMS\Log\Log; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Router\Route; use Joomla\Event\SubscriberInterface; @@ -19,8 +20,8 @@ use Moko\Plugin\System\MokoWaaS\Helper\MokoWaaSHelper; /** * MokoWaaS Firewall Plugin * - * Provides HTTPS enforcement, trusted IP management, admin session timeout, - * upload restrictions, and password policy enforcement. + * Web Application Firewall with security shields, IP management, + * request inspection, and access control. * * @since 02.32.00 */ @@ -28,6 +29,17 @@ class Firewall extends CMSPlugin implements SubscriberInterface { protected $autoloadLanguage = true; + private const BLOCKED_FILES = [ + 'htaccess.txt', 'web.config.txt', 'configuration.php-dist', + 'README.txt', 'LICENSE.txt', 'joomla.xml', 'robots.txt.dist', + ]; + + private const BLOCKED_PHP_DIRS = [ + '/images/', '/media/', '/tmp/', '/cache/', '/logs/', + ]; + + private const DEFAULT_MUA_BLOCKLIST = 'sqlmap,nikto,nmap,havij,w3af,acunetix,nessus,openvas,masscan,gobuster,dirbuster,wpscan,joomscan'; + public static function getSubscribedEvents(): array { return [ @@ -36,24 +48,447 @@ class Firewall extends CMSPlugin implements SubscriberInterface ]; } + // ================================================================== + // Main entry point + // ================================================================== + public function onAfterInitialise(): void { + $app = $this->getApplication(); + + if ($app->isClient('cli')) + { + return; + } + + $bypass = MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted(); + + // IP blocklist runs first — explicit deny even for trusted + $this->checkIpBlocklist(); + + // Admin secret + if ($app->isClient('administrator')) + { + $this->checkAdminSecret(); + } + + // WAF shields — skip for trusted/master + if (!$bypass && $this->params->get('waf_enabled', 1)) + { + $this->checkSqlInjection(); + $this->checkXss(); + $this->checkMaliciousUserAgent(); + $this->checkRemoteFileInclusion(); + $this->checkDirectFileInclusion(); + } + + // File/template protection — skip for trusted/master + if (!$bypass) + { + $this->checkBlockedFiles(); + $this->checkTemplateSwitch(); + $this->checkDirectPhpAccess(); + } + + // Existing features $this->enforceHttps(); $this->enforceUploadRestrictions(); - if ($this->getApplication()->isClient('administrator')) + if ($app->isClient('administrator')) { $this->enforceAdminSessionTimeout(); } } - /** - * Enforce password complexity rules before user save. - */ + // ================================================================== + // WAF Shields + // ================================================================== + + private function checkSqlInjection(): void + { + if (!$this->params->get('waf_sqli', 1)) + { + return; + } + + $pattern = '#' + . 'union\s+(all\s+)?select' + . '|\bor\b\s+\d+=\d+' + . '|\band\b\s+\d+=\d+' + . "|\bor\b\s+['\"][^'\"]*['\"]\\s*=\\s*['\"]" + . '|;\s*(drop|delete|insert|update|alter|create|truncate)\b' + . '|/\*.*?\*/' + . '|--\s' + . '|\b(benchmark|sleep|load_file|outfile|dumpfile)\s*\(' + . '|0x[0-9a-f]{8,}' + . '#i'; + + $match = $this->scanInput($_GET, $pattern) + ?? $this->scanInput($_POST, $pattern) + ?? $this->scanInput($_COOKIE, $pattern); + + if ($match !== null) + { + $this->logAndBlock('sqli', $match); + } + } + + private function checkXss(): void + { + if (!$this->params->get('waf_xss', 1)) + { + return; + } + + $pattern = '#' + . '<\s*script' + . '|javascript\s*:' + . '|vbscript\s*:' + . '|\bon\w+\s*=' + . '|<\s*(iframe|object|embed|applet|form)\b' + . '|document\s*\.\s*(cookie|domain)' + . '|\beval\s*\(' + . '|expression\s*\(' + . '#i'; + + $match = $this->scanInput($_GET, $pattern) + ?? $this->scanInput($_POST, $pattern); + + if ($match !== null) + { + $this->logAndBlock('xss', $match); + } + } + + private function checkMaliciousUserAgent(): void + { + if (!$this->params->get('waf_mua', 1)) + { + return; + } + + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + + if (empty($ua)) + { + return; + } + + $blocklist = $this->params->get('waf_mua_blocklist', self::DEFAULT_MUA_BLOCKLIST); + $agents = array_filter(array_map('trim', explode(',', $blocklist))); + $uaLower = strtolower($ua); + + foreach ($agents as $agent) + { + if (!empty($agent) && str_contains($uaLower, strtolower($agent))) + { + $this->logAndBlock('mua', $agent); + } + } + } + + private function checkRemoteFileInclusion(): void + { + if (!$this->params->get('waf_rfi', 1)) + { + return; + } + + $pattern = '#https?://|ftp://|php://|data://|expect://|%00#i'; + $match = $this->scanInput($_GET, $pattern); + + if ($match !== null) + { + $this->logAndBlock('rfi', $match); + } + } + + private function checkDirectFileInclusion(): void + { + if (!$this->params->get('waf_dfi', 1)) + { + return; + } + + $pattern = '#\.\.[/\\\\]|/etc/(passwd|shadow|hosts)|[A-Z]:\\\\(windows|winnt)|php://(filter|input)#i'; + $match = $this->scanInput($_GET, $pattern); + + if ($match !== null) + { + $this->logAndBlock('dfi', $match); + } + } + + // ================================================================== + // File & Template Protection + // ================================================================== + + private function checkBlockedFiles(): void + { + if (!$this->params->get('block_sensitive_files', 1)) + { + return; + } + + $path = strtolower(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''); + + foreach (self::BLOCKED_FILES as $file) + { + if (str_ends_with($path, '/' . strtolower($file))) + { + $this->logAndBlock('blocked_file', $file); + } + } + } + + private function checkDirectPhpAccess(): void + { + if (!$this->params->get('block_direct_php', 1)) + { + return; + } + + $path = strtolower(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''); + + if (!str_ends_with($path, '.php')) + { + return; + } + + foreach (self::BLOCKED_PHP_DIRS as $dir) + { + if (str_contains($path, strtolower($dir))) + { + $this->logAndBlock('blocked_php', $path); + } + } + } + + private function checkTemplateSwitch(): void + { + if (!$this->params->get('block_template_switch', 1)) + { + return; + } + + $tmpl = $_GET['tmpl'] ?? ''; + $template = $_GET['template'] ?? ''; + + if (!empty($tmpl) && $tmpl !== 'component') + { + $this->logAndBlock('tmpl_switch', 'tmpl=' . $tmpl); + } + + if (!empty($template)) + { + $this->logAndBlock('tmpl_switch', 'template=' . $template); + } + } + + // ================================================================== + // Access Control + // ================================================================== + + private function checkIpBlocklist(): void + { + $entries = $this->params->get('ip_blocklist', ''); + + if (empty($entries)) + { + return; + } + + if (\is_string($entries)) + { + $entries = json_decode($entries, true); + } + + if (!\is_array($entries)) + { + return; + } + + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + + if ($this->ipMatchesList($ip, $entries)) + { + $this->logAndBlock('ip_blocklist', $ip); + } + } + + private function checkAdminSecret(): void + { + $secret = $this->params->get('admin_secret', ''); + + if (empty($secret)) + { + return; + } + + if (MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted()) + { + return; + } + + $provided = $_GET['secret'] ?? ''; + + if ($provided === $secret) + { + Factory::getSession()->set('mokowaas.admin_secret_ok', true); + + return; + } + + if (Factory::getSession()->get('mokowaas.admin_secret_ok', false)) + { + return; + } + + $redirect = $this->params->get('admin_secret_redirect', ''); + + if (!empty($redirect)) + { + $this->getApplication()->redirect($redirect); + } + else + { + $this->logAndBlock('admin_secret', 'missing or invalid'); + } + } + + // ================================================================== + // Logging + // ================================================================== + + private function logAndBlock(string $rule, string $detail): void + { + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; + $uri = $_SERVER['REQUEST_URI'] ?? ''; + + // Log to database (best-effort — don't let log failures prevent the block) + try + { + $db = Factory::getDbo(); + $row = (object) [ + 'ip' => substr($ip, 0, 45), + 'uri' => substr($uri, 0, 2048), + 'rule' => substr($rule, 0, 50), + 'detail' => substr($detail, 0, 512), + 'user_agent' => substr($ua, 0, 512), + 'created' => gmdate('Y-m-d H:i:s'), + ]; + $db->insertObject('#__mokowaas_waf_log', $row); + } + catch (\Throwable $e) + { + // Silent — blocking is more important than logging + } + + // Hard 403 — bypass Joomla's response stack to avoid boot-order issues + http_response_code(403); + header('Content-Type: text/html; charset=utf-8'); + echo '403 Forbidden' + . '

403 Forbidden

Your request has been blocked by the security firewall.

'; + exit; + } + + // ================================================================== + // Input Scanning + // ================================================================== + + private function scanInput(array $input, string $pattern): ?string + { + foreach ($input as $key => $value) + { + if (\is_array($value)) + { + $match = $this->scanInput($value, $pattern); + + if ($match !== null) + { + return $match; + } + + continue; + } + + $value = (string) $value; + $decoded = urldecode($value); + + if (preg_match($pattern, $value) || preg_match($pattern, $decoded)) + { + return substr($value, 0, 200); + } + + if (preg_match($pattern, (string) $key)) + { + return substr((string) $key, 0, 200); + } + } + + return null; + } + + private function ipMatchesList(string $ip, array $entries): bool + { + $ipLong = ip2long($ip); + + if ($ipLong === false) + { + return false; + } + + foreach ($entries as $entry) + { + if (empty($entry['enabled']) || empty($entry['ip'])) + { + continue; + } + + $range = trim($entry['ip']); + + if (str_contains($range, '*')) + { + $pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/'; + + if (preg_match($pattern, $ip)) + { + return true; + } + + continue; + } + + if (str_contains($range, '/')) + { + [$subnet, $bits] = explode('/', $range, 2); + $subnetLong = ip2long($subnet); + $mask = -1 << (32 - (int) $bits); + + if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask)) + { + return true; + } + + continue; + } + + if ($ip === $range) + { + return true; + } + } + + return false; + } + + // ================================================================== + // Existing Features + // ================================================================== + public function onUserBeforeSave($event): void { - $oldUser = $event[0] ?? $event->getArgument(0, []); - $isNew = $event[1] ?? $event->getArgument(1, false); $newUser = $event[2] ?? $event->getArgument(2, []); if (empty($newUser['password_clear'])) @@ -91,9 +526,6 @@ class Firewall extends CMSPlugin implements SubscriberInterface } } - /** - * Redirect non-HTTPS requests to HTTPS. - */ private function enforceHttps(): void { if (!$this->params->get('force_https', 0)) @@ -117,9 +549,6 @@ class Firewall extends CMSPlugin implements SubscriberInterface } } - /** - * Enforce admin session idle timeout. - */ private function enforceAdminSessionTimeout(): void { $timeout = (int) $this->params->get('admin_session_timeout', 0); @@ -129,12 +558,7 @@ class Firewall extends CMSPlugin implements SubscriberInterface return; } - if (MokoWaaSHelper::isMasterUser()) - { - return; - } - - if ($this->ipIsTrusted()) + if (MokoWaaSHelper::isMasterUser() || $this->ipIsTrusted()) { return; } @@ -154,9 +578,6 @@ class Firewall extends CMSPlugin implements SubscriberInterface $session->set('mokowaas.last_activity', $now); } - /** - * Check whether the current request IP matches any trusted IP entry. - */ private function ipIsTrusted(): bool { $entries = $this->params->get('trusted_ips', ''); @@ -176,64 +597,9 @@ class Firewall extends CMSPlugin implements SubscriberInterface return false; } - $ip = $_SERVER['REMOTE_ADDR'] ?? ''; - $ipLong = ip2long($ip); - - if ($ipLong === false) - { - return false; - } - - foreach ($entries as $entry) - { - if (empty($entry['enabled']) || empty($entry['ip'])) - { - continue; - } - - $range = trim($entry['ip']); - - // Wildcard: 192.168.1.* - if (str_contains($range, '*')) - { - $pattern = '/^' . str_replace(['.', '*'], ['\\.', '\\d+'], $range) . '$/'; - - if (preg_match($pattern, $ip)) - { - return true; - } - - continue; - } - - // CIDR: 10.0.0.0/8 - if (str_contains($range, '/')) - { - [$subnet, $bits] = explode('/', $range, 2); - $subnetLong = ip2long($subnet); - $mask = -1 << (32 - (int) $bits); - - if ($subnetLong !== false && ($ipLong & $mask) === ($subnetLong & $mask)) - { - return true; - } - - continue; - } - - // Exact match - if ($ip === $range) - { - return true; - } - } - - return false; + return $this->ipMatchesList($_SERVER['REMOTE_ADDR'] ?? '', $entries); } - /** - * Override Joomla upload restrictions at runtime. - */ private function enforceUploadRestrictions(): void { $types = $this->params->get('upload_allowed_types', '');