feat: Web Application Firewall with 10 security shields (#122)

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) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-02 09:59:49 -05:00
parent d3ec76dc0f
commit f21bcdd6bb
6 changed files with 648 additions and 90 deletions
@@ -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' => [
@@ -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 &amp; 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 &amp; 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."
@@ -8,16 +8,24 @@
<license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl>
<version>02.32.09</version>
<version>02.32.00</version>
<description>PLG_SYSTEM_MOKOWAAS_FIREWALL_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoWaaSFirewall</namespace>
<files>
<folder>src</folder>
<folder>sql</folder>
<folder>services</folder>
<folder>language</folder>
</files>
<install>
<sql><file driver="mysql" charset="utf8">sql/install.mysql.sql</file></sql>
</install>
<uninstall>
<sql><file driver="mysql" charset="utf8">sql/uninstall.mysql.sql</file></sql>
</uninstall>
<languages folder="language">
<language tag="en-GB">en-GB/plg_system_mokowaas_firewall.ini</language>
<language tag="en-GB">en-GB/plg_system_mokowaas_firewall.sys.ini</language>
@@ -25,6 +33,7 @@
<config>
<fields name="params">
<!-- Network & Session -->
<fieldset name="basic"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_BASIC_DESC">
@@ -52,6 +61,137 @@
buttons="add,remove,move" />
</fieldset>
<!-- WAF Shields -->
<fieldset name="waf"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_WAF_DESC">
<field name="waf_enabled" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_ENABLED_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_sqli" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_SQLI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_xss" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_XSS_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_mua" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_mua_blocklist" type="textarea"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_MUA_LIST_DESC"
rows="4" filter="raw"
default="sqlmap,nikto,nmap,havij,w3af,acunetix,nessus,openvas,masscan,gobuster,dirbuster,wpscan,joomscan"
showon="waf_enabled:1[AND]waf_mua:1" />
<field name="waf_rfi" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_RFI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="waf_dfi" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_WAF_DFI_DESC"
class="btn-group btn-group-yesno"
showon="waf_enabled:1">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- Access Control -->
<fieldset name="access_control"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_ACCESS_DESC">
<field name="ip_blocklist" type="subform"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_IP_BLOCKLIST_DESC"
formsource="plugins/system/mokowaas/forms/trusted_ip_entry.xml"
multiple="true"
layout="joomla.form.field.subform.repeatable-table"
groupByFieldset="false"
buttons="add,remove,move" />
<field name="admin_secret" type="text"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_DESC"
default="" filter="raw" hint="Leave empty to disable" />
<field name="admin_secret_redirect" type="text"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_ADMIN_SECRET_REDIRECT_DESC"
default="" filter="url" hint="Empty = 403 Forbidden"
showon="admin_secret!:" />
<field name="block_frontend_superuser" type="radio" default="0"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FE_SU_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- File & Template Protection -->
<fieldset name="protection"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PROTECTION_DESC">
<field name="block_sensitive_files" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_FILES_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="block_direct_php" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_PHP_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="block_template_switch" type="radio" default="1"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_LABEL"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_BLOCK_TMPL_DESC"
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
</fieldset>
<!-- Password Policy -->
<fieldset name="password_policy"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_PASSWORD_DESC">
@@ -82,6 +222,7 @@
</field>
</fieldset>
<!-- Upload Restrictions -->
<fieldset name="uploads"
label="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS"
description="PLG_SYSTEM_MOKOWAAS_FIREWALL_FIELDSET_UPLOADS_DESC">
@@ -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;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS `#__mokowaas_waf_log`;
@@ -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 '<!DOCTYPE html><html><head><title>403 Forbidden</title></head>'
. '<body><h1>403 Forbidden</h1><p>Your request has been blocked by the security firewall.</p></body></html>';
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', '');