Phase 6: AIOS security plugin with conservative login lockdown config (10 attempts)

This commit is contained in:
Hanson.xyz Dev
2025-11-28 17:19:54 -06:00
parent 78a744ef06
commit abbd3502e8
430 changed files with 137111 additions and 7 deletions
@@ -0,0 +1,18 @@
<?php
namespace AIOWPS\Firewall;
/**
* Trait which exits the current request
*/
trait Action_Exit_Trait {
/**
* Exit when the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
Event::raise('action_before_exit');
exit();
}
}
@@ -0,0 +1,23 @@
<?php
namespace AIOWPS\Firewall;
/**
* Combines the forbid and exit trait
*/
trait Action_Forbid_and_Exit_Trait {
use Action_Forbid_Trait, Action_Exit_Trait {
Action_Forbid_Trait::do_action as protected do_action_forbid;
Action_Exit_Trait::do_action as protected do_action_exit;
}
/**
* Forbid 403 and Exit when the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
$this->do_action_forbid();
$this->do_action_exit();
}
}
@@ -0,0 +1,17 @@
<?php
namespace AIOWPS\Firewall;
/**
* Trait to set the header to forbidden
*/
trait Action_Forbid_Trait {
/**
* Forbid 403 when the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
header('HTTP/1.1 403 Forbidden');
}
}
@@ -0,0 +1,88 @@
<?php
namespace AIOWPS\Firewall;
trait Action_Permblock_and_Exit_Trait {
/**
* Use the forbid and exit trait
*/
use Action_Forbid_and_Exit_Trait {
Action_Forbid_and_Exit_Trait::do_action as protected do_action_forbid_and_exit;
}
/**
* Holds the reason for the perm. block
*
* @var string
*/
private $permblock_reason = '';
/**
* Holds the IP for the perm. block
*
* @var string
*/
private $permblock_ip = '';
/**
* Sets the reason for the perm. block
*
* @param string $reason
* @return void
*/
public function set_perm_block_reason($reason) {
$this->permblock_reason = $reason;
}
/**
* Sets the IP for the perm. block
*
* @param string $ip
* @return void
*/
public function set_perm_block_ip($ip) {
$this->permblock_ip = $ip;
}
/**
* Permanently ban the IP and exit when the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
if (!Utility::attempt_to_access_wpdb()) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- PCP warning. Part of AIOS error reporting system.
error_log('AIOS: Unable to access wpdb to ban IP address.');
$this->do_action_forbid_and_exit();
}
global $wpdb;
$table = $wpdb->prefix.'aiowps_permanent_block';
$ip = empty($this->permblock_ip) ? \AIOS_Helper::get_user_ip_address() : $this->permblock_ip;
$data = array(
'blocked_ip' => $ip,
'block_reason' => empty($this->permblock_reason) ? 'firewall_generic' : $this->permblock_reason,
'blocked_date' => current_time('mysql')
);
// Check if the IP already exists
// phpcs:ignore WordPress.DB.PreparedSQL, WordPress.DB.DirectDatabaseQuery -- PCP error. Table name cannot be done via prepare.
$already_exists = $wpdb->get_var($wpdb->prepare("SELECT blocked_ip FROM `{$table}` WHERE blocked_ip = %s", $ip));
// If it does exist, no point adding it again so just forbid and exit
if (!is_null($already_exists)) $this->do_action_forbid_and_exit();
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Table name cannot be done via prepare.
if (false === $wpdb->query($wpdb->prepare("INSERT INTO " .$table." (blocked_ip, block_reason, blocked_date, created) VALUES (%s, %s, %s, UNIX_TIMESTAMP())", $data['blocked_ip'], $data['block_reason'], $data['blocked_date']))) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- PCP warning. Needed for error reporting.
error_log('AIOS: Unable to insert IP address into table.');
}
$this->do_action_forbid_and_exit();
}
}
@@ -0,0 +1,23 @@
<?php
namespace AIOWPS\Firewall;
/**
* Combines the redirect and exit trait
*/
trait Action_Redirect_and_Exit_Trait {
use Action_Redirect_Trait, Action_Exit_Trait {
Action_Redirect_Trait::do_action as protected do_action_redirect;
Action_Exit_Trait::do_action as protected do_action_exit;
}
/**
* Redirect and Exit when the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
$this->do_action_redirect();
$this->do_action_exit();
}
}
@@ -0,0 +1,24 @@
<?php
namespace AIOWPS\Firewall;
/**
* Trait to set the header to redirect
*/
trait Action_Redirect_Trait {
/**
* Redirect to the location.
*
* @var string
*/
public $location = '127.0.0.1';
/**
* Redirect the rule condition is satisfied.
*
* @return void
*/
public function do_action() {
header("Location: $this->location");
}
}
@@ -0,0 +1,62 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain kinds of data from the query string
*/
class Rule_Block_Query_Strings_6g extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Block query strings';
$this->family = '6G';
$this->priority = 0;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_6g_block_query');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['QUERY_STRING'])) return Rule::NOT_SATISFIED;
//Patterns to match against
$patterns = array(
'/[a-z0-9]{2000,}/i',
'/(eval\()/i',
'/(127\.0\.0\.1)/i',
'/(javascript:)(.*)(;)/i',
'/(base64_encode)(.*)(\()/i',
'/(GLOBALS|REQUEST)(=|\[|%)/i',
'/(<|%3C)(.*)script(.*)(>|%3)/i',
'#(\|\.\.\.|\.\./|~|`|<|>|\|)#i',
'#(boot\.ini|etc/passwd|self/environ)#i',
'/(thumbs?(_editor|open)?|tim(thumb)?)\.php/i',
'/(\'|\")(.*)(drop|insert|md5|select|union)/i',
);
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
return Rule_Utils::contains_pattern(rawurldecode($_SERVER['QUERY_STRING']), $patterns);
}
}
@@ -0,0 +1,52 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain referrers recommended by 6G
*/
class Rule_Block_Refs_6g extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Block referrer strings';
$this->family = '6G';
$this->priority = 0;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_6g_block_referrers');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['HTTP_REFERER'])) return Rule::NOT_SATISFIED;
//Patterns to match against
$patterns = array(
'/[a-z0-9]{2000,}/i',
'/(semalt.com|todaperfeita)/i',
);
return Rule_Utils::contains_pattern($_SERVER['HTTP_REFERER'], $patterns); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is not a WordPress context. Also this only evaluates to a boolean.
}
}
@@ -0,0 +1,67 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain kinds of data from the request string
*/
class Rule_Block_Request_Strings_6g extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Block request strings';
$this->family = '6G';
$this->priority = 0;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_6g_block_request');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['REQUEST_URI'])) return Rule::NOT_SATISFIED;
// ensure we get the request uri without the query string
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
$uri = (string) parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ('' == $uri) return Rule::NOT_SATISFIED;
//Patterns to match against
$patterns = array(
'/[a-z0-9]{2000,}/i',
'#(https?|ftp|php):/#i',
'#(base64_encode)(.*)(\()#i',
'#(=\'|=\%27|/\'/?)\.#i',
'#/(\$(\&)?|\*|\"|\.|,|&|&amp;?)/?$#i',
'#(\{0\}|\(/\(|\.\.\.|\+\+\+|\\"\\")#i',
'#(~|`|<|>|:|;|,|%|\|\s|\{|\}|\[|\]|\|)#i',
'#/(=|\$&|_mm|cgi-|etc/passwd|muieblack)#i',
'#(&pws=0|_vti_|\(null\)|\{\$itemURL\}|echo(.*)kae|etc/passwd|eval\(|self/environ)#i',
'#\.(aspx?|bash|bak?|cfg|cgi|dll|exe|git|hg|ini|jsp|log|mdb|out|sql|svn|swp|tar|rar|rdf)$#i',
'#/(^$|(wp-)?config|mobiquo|phpinfo|shell|sqlpatch|thumb|thumb_editor|thumbopen|timthumb|webshell)\.php#i',
);
return Rule_Utils::contains_pattern(rawurldecode($uri), $patterns);
}
}
@@ -0,0 +1,53 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain user-agents recommended by 6G
*/
class Rule_Block_User_Agents_6g extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Block user-agents';
$this->family = '6G';
$this->priority = 0;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_6g_block_agents');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['HTTP_USER_AGENT'])) return Rule::NOT_SATISFIED;
//Patterns to match against
$patterns = array(
'/[a-z0-9]{2000,}/i',
'/(archive.org|binlar|casper|checkpriv|choppy|clshttp|cmsworld|diavol|dotbot|extract|feedfinder|flicky|g00g1e|harvest|heritrix|httrack|kmccrew|loader|miner|nikto|nutch|planetwork|postrank|purebot|pycurl|python|seekerspider|siclab|skygrid|sqlmap|sucker|turnit|vikspider|winhttp|xxxyy|youda|zmeu|zune)/i',
);
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
return Rule_Utils::contains_pattern($_SERVER['HTTP_USER_AGENT'], $patterns);
}
}
@@ -0,0 +1,53 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain kinds of HTTP request methods (e.g DEBUG or PUT)
*/
class Rule_Request_Method_6g extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* List of request methods to block
*
* @var array
*/
private $blocked_methods;
/**
* Construct our rule
*/
public function __construct() {
global $aiowps_firewall_config;
// Set the rule's metadata
$this->name = 'Block request methods';
$this->family = '6G';
$this->priority = 0;
$this->blocked_methods = $aiowps_firewall_config->get_value('aiowps_6g_block_request_methods');
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
return !empty($this->blocked_methods);
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
return isset($_SERVER['REQUEST_METHOD']) && in_array(strtoupper($_SERVER['REQUEST_METHOD']), $this->blocked_methods);
}
}
@@ -0,0 +1,65 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks IPs to access.
*/
class Rule_Ips_Blacklist extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* List of IPs / IP range to block
*
* @var array
*/
private $blocked_ips;
/**
* Construct our rule
*
* @global Config $aiowps_firewall_config
*/
public function __construct() {
global $aiowps_firewall_config;
// Set the rule's metadata
$this->name = 'Blocked IPs';
$this->family = 'Blacklist';
$this->priority = 0;
$this->blocked_ips = $aiowps_firewall_config->get_value('aiowps_blacklist_ips');
}
/**
* Determines whether the rule is active
*
* @global Constants $aiowps_firewall_constants
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_constants;
if ($aiowps_firewall_constants->AIOS_DISABLE_BLACKLIST_IP_MANAGER) {
return false;
} else {
return !empty($this->blocked_ips);
}
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
$user_ip_blocked = \AIOS_Helper::is_user_ip_address_within_list($this->blocked_ips);
if (true == $user_ip_blocked) return Rule::SATISFIED;
return Rule::NOT_SATISFIED;
}
}
@@ -0,0 +1,57 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks user agents to access.
*/
class Rule_User_Agent_Blacklist extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* List of user agents to block
*
* @var array
*/
private $blocked_user_agents;
/**
* Construct our rule
*/
public function __construct() {
global $aiowps_firewall_config;
// Set the rule's metadata
$this->name = 'Blocked user agents';
$this->family = 'Blacklist';
$this->priority = 0;
$this->blocked_user_agents = $aiowps_firewall_config->get_value('aiowps_blacklist_user_agents');
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
return !empty($this->blocked_user_agents) && isset($_SERVER['HTTP_USER_AGENT']);
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
foreach ($this->blocked_user_agents as $block_user_agent) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
if (isset($_SERVER['HTTP_USER_AGENT']) && !empty($block_user_agent) && false !== stripos($_SERVER['HTTP_USER_AGENT'], $block_user_agent)) {
return Rule::SATISFIED;
}
}
return Rule::NOT_SATISFIED;
}
}
@@ -0,0 +1,44 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that bans the IP address if the POST request has a blank user-agent and referer
*/
class Rule_Ban_Post_Blank_Headers extends Rule {
/**
* Implements the action to be taken
*/
use Action_Permblock_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Ban POST requests with blank user-agent and referer';
$this->family = 'Bots';
$this->priority = 10;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_ban_post_blank_headers');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
$this->set_perm_block_reason('firewall_post_blank_user_agent_and_referer');
return isset($_SERVER['REQUEST_METHOD']) && (0 === strcasecmp($_SERVER['REQUEST_METHOD'], "POST")) && empty($_SERVER['HTTP_USER_AGENT']) && empty($_SERVER['HTTP_REFERER']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- This is not a WordPress context. Also this only evaluates to a boolean.
}
}
@@ -0,0 +1,101 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks fake Googlebots.
*/
class Rule_Block_Fake_Googlebots extends Rule {
/**
* Implements the action to be taken.
*/
use Action_Exit_Trait;
/**
* Construct our rule.
*/
public function __construct() {
// Set the rule's metadata.
$this->name = 'Block fake Googlebots';
$this->family = 'Bots';
$this->priority = 0;
}
/**
* Determines whether the rule is active.
*
* @global Config $aiowps_firewall_config
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_block_fake_googlebots');
}
/**
* The condition to be satisfied for the rule to apply.
*
* @global Config $aiowps_firewall_config
*
* @return boolean
*/
public function is_satisfied() {
global $aiowps_firewall_config;
$user_agent = (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '');
if (preg_match('/Googlebot/i', $user_agent, $matches)) {
// If the user agent says it's a Googlebot, start doing checks.
$ip = \AIOS_Helper::get_user_ip_address();
if (empty($ip)) {
return Rule::NOT_SATISFIED;
}
try {
$name = gethostbyaddr($ip); // Let's get the hostname using the IP address.
if ($name == $ip || false === $name) {
// gethostbyaddr failed.
$googlebot_ips = $aiowps_firewall_config->get_value('aiowps_googlebot_ip_ranges');
if (\AIOS_Helper::is_user_ip_address_within_list($googlebot_ips)) {
return Rule::NOT_SATISFIED;
} else {
return Rule::SATISFIED;
}
}
$host_ip = gethostbyname($name); // Reverse lookup - let's get the IP address using the hostname.
} catch (\Exception $e) {
// gethostbyaddr or gethostbyname not available on site.
$googlebot_ips = $aiowps_firewall_config->get_value('aiowps_googlebot_ip_ranges');
if (\AIOS_Helper::is_user_ip_address_within_list($googlebot_ips)) {
return Rule::NOT_SATISFIED;
} else {
return Rule::SATISFIED;
}
} catch (\Error $e) { // phpcs:ignore PHPCompatibility.Classes.NewClasses.errorFound -- this won't run on PHP 5.6 so we still want to catch it on other versions
// gethostbyaddr or gethostbyname not available on site.
$googlebot_ips = $aiowps_firewall_config->get_value('aiowps_googlebot_ip_ranges');
if (\AIOS_Helper::is_user_ip_address_within_list($googlebot_ips)) {
return Rule::NOT_SATISFIED;
} else {
return Rule::SATISFIED;
}
}
if (preg_match('/^(?:.+\.)?googlebot\.com$/i', $name) || preg_match('/^(?:.+\.)?google\.com$/i', $name) || preg_match('/^(?:.+\.)?googleusercontent\.com$/i', $name)) {
if ($host_ip == $ip) {
return Rule::NOT_SATISFIED;
} else {
return Rule::SATISFIED;
}
} else {
return Rule::SATISFIED;
}
}
}
}
@@ -0,0 +1,98 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that uses a cookie to prevent bruteforce attacks.
*/
class Rule_Cookie_Prevent_Bruteforce extends Rule {
/**
* Implements the action to be taken
*/
use Action_Redirect_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Cookie based prevent bruteforce';
$this->family = 'Bruteforce';
$this->priority = 0;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config, $aiowps_firewall_constants;
if ($aiowps_firewall_constants->AIOS_DISABLE_COOKIE_BRUTE_FORCE_PREVENTION) {
return false;
} else {
return (bool) $aiowps_firewall_config->get_value('aios_enable_brute_force_attack_prevention');
}
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
global $aiowps_firewall_config;
/**
* This rule is not applied at AIOS plugin activation time.
*/
$is_plugins_page = isset($_SERVER['SCRIPT_FILENAME']) && 1 === preg_match('#/wp-admin/(network/)?plugins\.php$#i', $_SERVER['SCRIPT_FILENAME']);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PCP warning. A nonce is not available at this point.
$is_activation_action = isset($_GET['action']) && 'activate' === $_GET['action'];
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PCP warning. A nonce is not available at this point.
$is_target_plugin = isset($_GET['plugin']) && 'all-in-one-wp-security-and-firewall/wp-security.php' === $_GET['plugin'];
if ($is_plugins_page && $is_activation_action && $is_target_plugin) {
return Rule::NOT_SATISFIED;
}
$brute_force_secret_word = $aiowps_firewall_config->get_value('aios_brute_force_secret_word');
$brute_force_secret_cookie_name = $aiowps_firewall_config->get_value('aios_brute_force_secret_cookie_name');
$login_page_slug = $aiowps_firewall_config->get_value('aios_login_page_slug');
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PCP warning. A nonce is not available at this point.
if (!isset($_GET[$brute_force_secret_word])) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing is not required, as we validate the raw input.
$brute_force_secret_cookie_val = isset($_COOKIE[$brute_force_secret_cookie_name]) ? $_COOKIE[$brute_force_secret_cookie_name] : '';
$pw_protected_exception = $aiowps_firewall_config->get_value('aios_brute_force_attack_prevention_pw_protected_exception');
$prevent_ajax_exception = $aiowps_firewall_config->get_value('aios_brute_force_attack_prevention_ajax_exception');
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing is not required, as we validate the raw input.
if (!empty($_SERVER['REQUEST_URI']) && !hash_equals($brute_force_secret_cookie_val, \AIOS_Helper::get_hash($brute_force_secret_word))) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing is not required, as we validate the raw input.
$request_uri = (string) parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// admin section or login page or login custom slug called
$is_admin_or_login = (false != strpos($request_uri, 'wp-admin') || false != strpos($request_uri, 'wp-login') || ('' != $login_page_slug && false != strpos($request_uri, $login_page_slug))) ? 1 : 0;
// admin side ajax called
$is_admin_ajax_request = ('1' == $prevent_ajax_exception && isset($_SERVER['SCRIPT_NAME']) && ('admin-ajax.php' === basename($_SERVER['SCRIPT_NAME']))) ? 1 : 0;
// password protected page called
$is_password_protected_access = ('1' == $pw_protected_exception && isset($_GET['action']) && 'postpass' == $_GET['action']) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PCP warning. A nonce is not available at this point.
// logout, set password, reset password action called
$is_logout_resetpassword_action = (isset($_GET['action']) && ('logout' == $_GET['action'] || 'rp' == $_GET['action'] || 'resetpass' == $_GET['action'])) ? 1 : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- PCP warning. A nonce is not available at this point.
// cookie based brute force on and accessing admin without ajax and password protected then redirect
if ($is_admin_or_login && !$is_admin_ajax_request && !$is_password_protected_access && !$is_logout_resetpassword_action) {
$redirect_url = $aiowps_firewall_config->get_value('aios_cookie_based_brute_force_redirect_url');
$this->location = $redirect_url;
return Rule::SATISFIED;
}
}
}
return Rule::NOT_SATISFIED;
}
}
@@ -0,0 +1,156 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain kinds of data from the request string
*/
class Rule_Advanced_Character_Filter extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Advanced character filter';
$this->family = 'General';
$this->priority = 10;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_advanced_char_string_filter');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['REQUEST_URI'])) return Rule::NOT_SATISFIED;
// ensure we get the request uri without the query string
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
$uri = (string) parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
return Rule_Utils::contains_pattern($uri, array_merge($this->get_general_characters(), $this->get_common_patterns(), $this->get_specific_exploits()));
}
/**
* Get the list of 'specific exploits' patterns
*
* @return array
*/
private function get_specific_exploits() {
return array(
'/errors\./i',
'/config\./i',
'/include\./i',
'/display\./i',
'/register\./i',
'/password\./i',
'/maincore\./i',
'/authorize\./i',
'/macromates\./i',
'/head\_auth\./i',
'/submit\_links\./i',
'/change\_action\./i',
'/com\_facileforms\//i',
'/admin\_db\_utilities\./i',
'/admin\.webring\.docs\./i',
'/Table\/Latest\/index\./i',
);
}
/**
* Get the list of common patterns
*
* @return array
*/
private function get_common_patterns() {
return array(
'/\_vpi/i',
'/\.inc/i',
'/xAou6/i',
'/db\_name/i',
'/select\(/i',
'/convert\(/i',
'/\/query\//i',
'/ImpEvData/i',
'/\.XMLHTTP/i',
'/proxydeny/i',
'/function\./i',
'/remoteFile/i',
'/servername/i',
'/\&rptmode\=/i',
'/sys\_cpanel/i',
'/db\_connect/i',
'/doeditconfig/i',
'/check\_proxy/i',
'/system\_user/i',
'/\/\(null\)\//i',
'/clientrequest/i',
'/option\_value/i',
'/ref\.outcontrol/i',
);
}
/**
* Get the list of general characters
*
* @return array
*/
private function get_general_characters() {
return array(
'/\,/i',
'/\:/i',
'/\;/i',
'/\=/i',
'/\[/i',
'/\]/i',
'/\^/i',
'/\`/i',
'/\{/i',
'/\}/i',
'/\~/i',
'/\"/i',
'/\$/i',
'/\</i',
'/\>/i',
'/\|/i',
'/\.\./i',
'/\%0/i',
'/\%A/i',
'/\%B/i',
'/\%C/i',
'/\%D/i',
'/\%E/i',
'/\%F/i',
'/\%22/i',
'/\%27/i',
'/\%28/i',
'/\%29/i',
'/\%3C/i',
'/\%3E/i',
'/\%3F/i',
'/\%5B/i',
'/\%5C/i',
'/\%5D/i',
'/\%7B/i',
'/\%7C/i',
'/\%7D/i',
);
}
}
@@ -0,0 +1,56 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks certain data from the URL's query string
*/
class Rule_Bad_Query_Strings extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Bad query strings';
$this->family = 'General';
$this->priority = 10;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_deny_bad_query_strings');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
if (empty($_SERVER['QUERY_STRING'])) return Rule::NOT_SATISFIED;
$patterns = array(
'/ftp:/i',
'/http:/i',
'/https:/i',
'/mosConfig/i',
'/^.*(globals|encode|loopback).*/i',
"/(\;|'|\"|%22).*(request|insert|union|declare|drop)/i",
);
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
return Rule_Utils::contains_pattern($_SERVER['QUERY_STRING'], $patterns);
}
}
@@ -0,0 +1,44 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks access to the xmlrpc.php file
*/
class Rule_Block_Xmlrpc extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Completely block XMLRPC';
$this->family = 'General';
$this->priority = 10;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_enable_pingback_firewall');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
return (isset($_SERVER['SCRIPT_FILENAME']) && 1 === preg_match('/\/xmlrpc\.php$/i', $_SERVER['SCRIPT_FILENAME']));
}
}
@@ -0,0 +1,69 @@
<?php
namespace AIOWPS\Firewall;
/**
* Rule that blocks comments being posted if a proxy is detected.
*/
class Rule_Proxy_Comment_Posting extends Rule {
/**
* Implements the action to be taken
*/
use Action_Forbid_and_Exit_Trait;
/**
* Construct our rule
*/
public function __construct() {
// Set the rule's metadata
$this->name = 'Proxy comment posting';
$this->family = 'General';
$this->priority = 10;
}
/**
* Determines whether the rule is active
*
* @return boolean
*/
public function is_active() {
global $aiowps_firewall_config;
return (bool) $aiowps_firewall_config->get_value('aiowps_forbid_proxy_comments');
}
/**
* The condition to be satisfied for the rule to apply
*
* @return boolean
*/
public function is_satisfied() {
//Preconditions for the rule
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
$is_comment_form = (isset($_SERVER['SCRIPT_FILENAME']) && 1 === preg_match('/\/wp-comments-post\.php$/i', $_SERVER['SCRIPT_FILENAME']));
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PCP warning. Sanitizing will interfere with 6g rules.
$is_post = (isset($_SERVER['REQUEST_METHOD']) && 0 === strcasecmp($_SERVER['REQUEST_METHOD'], "POST"));
if (!$is_post || !$is_comment_form) return Rule::NOT_SATISFIED;
//Headers that are present if a proxy is being used
$headers = array(
'HTTP_VIA',
'HTTP_FORWARDED',
'HTTP_USERAGENT_VIA',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED_HOST',
'HTTP_PROXY_CONNECTION',
'HTTP_XPROXY_CONNECTION',
'HTTP_PC_REMOTE_ADDR',
'HTTP_CLIENT_IP',
);
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) return Rule::SATISFIED;
}
return Rule::NOT_SATISFIED;
}
}
@@ -0,0 +1,46 @@
<?php
namespace AIOWPS\Firewall;
/**
* Builds our rules
*/
class Rule_Builder {
/**
* Gets our rule if it's active
*
* @return iterable
*/
public static function get_active_rule() {
foreach (self::get_rule_classname() as $classname) {
$rule = new $classname();
if (!$rule->is_active()) {
Event::raise('rule_not_active', $rule, $classname);
continue;
}
Event::raise('rule_active', $rule, $classname);
yield $rule;
}
}
/**
* Generates the classname for each rule
*
* @return iterable
*/
private static function get_rule_classname() {
$rec_iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(AIOWPS_FIREWALL_DIR.'/rule/rules/', \FilesystemIterator::SKIP_DOTS));
foreach ($rec_iterator as $dir_iterator) {
$matches = array();
if (preg_match('/^rule-(?<rule_name>.*)\.php$/', $dir_iterator->getFilename(), $matches)) {
yield "AIOWPS\Firewall\Rule_".ucwords(str_replace('-', '_', $matches['rule_name']), '_');
}
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace AIOWPS\Firewall;
/**
* Utility methods to help with the rules
*/
class Rule_Utils {
/**
* Check if the subject contains the given pattern or patterns
*
* @param string $subject - The subject we wish to check the pattern or patterns against.
* @param string|array $pattern - Regex pattern. An array for multiple patterns; a string otherwise.
* @return boolean
*/
public static function contains_pattern($subject, $pattern) {
if (empty($subject)) return false;
if (is_string($pattern)) return (1 === preg_match($pattern, $subject));
if (!is_array($pattern)) return false;
foreach ($pattern as $patt) {
if (preg_match($patt, $subject)) return true;
}
return false;
}
}
@@ -0,0 +1,91 @@
<?php
namespace AIOWPS\Firewall;
/**
* Base class for our firewall rules
*/
abstract class Rule {
/**
* Name of the rule
*
* @var string
*/
public $name;
/**
* Name of the family the rule belongs to
*
* @var string
*/
public $family;
/**
* Rule's priority (0 is the highest)
*
* @var int
*/
public $priority;
/**
* An abstraction for when the rule is satisfied
*
* @var boolean
*/
const SATISFIED = true;
/**
* An abstraction for when the rule is not satisfied
*
* @var boolean
*/
const NOT_SATISFIED = false;
/**
* Executes the rule's action
*
* @return void
*/
abstract public function do_action();
/**
* Check if the rule is active
*
* @return boolean
*/
abstract public function is_active();
/**
* Check if the rule has been satisfied
*
* @return boolean
*/
abstract public function is_satisfied();
/**
* Apply the rule and execute the action if satisfied
*
* @return void
*/
public function apply() {
if ($this->is_satisfied()) {
Event::raise('rule_triggered', $this, time());
$this->do_action();
}
Event::raise('rule_not_triggered', $this, time());
}
/**
* Show the rule's name
*
* @return string
*/
public function __toString() {
return $this->name;
}
}