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:
@@ -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' => [
|
||||
|
||||
+46
-9
@@ -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."
|
||||
|
||||
@@ -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', '');
|
||||
|
||||
Reference in New Issue
Block a user