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,13 @@
<?php
namespace AIOWPS\Firewall;
/**
* Our list of families
*/
return array(
array('name' => '6G', 'priority' => 10),
array('name' => 'Blacklist', 'priority' => 1),
array('name' => 'Bruteforce', 'priority' => 0),
array('name' => 'General', 'priority' => 20),
array('name' => 'Bots', 'priority' => 2),
);
@@ -0,0 +1,33 @@
<?php
namespace AIOWPS\Firewall;
/**
* Builds all our families
*/
class Family_Builder {
/**
* Get our families sorted by priority
*
* @return array
*/
public static function get_families() {
$family_list = include(AIOWPS_FIREWALL_DIR.'/family/wp-security-firewall-families.php');
//Prioritise the families
usort($family_list, function($member, $member2) {
if ($member['priority'] == $member2['priority']) {
return 0;
}
return ($member['priority'] > $member2['priority']) ? 1 : -1;
});
$families = array();
foreach ($family_list as $member) {
$families[strtolower($member['name'])] = new Family($member['name'], $member['priority']);
}
return $families;
}
}
@@ -0,0 +1,49 @@
<?php
namespace AIOWPS\Firewall;
/**
* Holds all our families
*/
class Family_Collection {
/**
* Holds our families
*
* @var array
*/
protected $families;
/**
* Constructs our family collection object
*
* @param array $families - The sorted families to contain
*/
public function __construct($families = array()) {
$this->families = $families;
}
/**
* Generator method to iterate over the families
*
* @return iterable
*/
public function get_family() {
foreach ($this->families as $family) {
yield $family;
}
}
/**
* Adds a new rule to a family member
*
* @param Rule $rule - an active rule to add to its family
* @return void
*/
public function add_rule_to_member(Rule $rule) {
$key = strtolower($rule->family);
if (array_key_exists($key, $this->families)) {
$this->families[$key]->add_rule($rule);
}
}
}
@@ -0,0 +1,86 @@
<?php
namespace AIOWPS\Firewall;
/**
* Represents a family (a grouping of rules)
*/
class Family {
/**
* Name of the family
*
* @var string
*/
public $name;
/**
* Priority of the family (0 is the highest)
*
* @var int
*/
public $priority;
/**
* List of rules to apply
*
* @var array
*/
protected $rules;
/**
* Builds our family object
*
* @param string $name
* @param integer $priority
*/
public function __construct($name, $priority = 999999) {
$this->name = $name;
$this->priority = $priority;
$this->rules = array();
}
/**
* Adds a rule to the family
*
* @param Rule $rule
* @return void
*/
public function add_rule(Rule $rule) {
$this->rules[] = $rule;
}
/**
* Applies all the rules in the family
*
* @return void
*/
public function apply_all() {
if (empty($this->rules)) {
return;
}
//ensure the rules are ordered by priority
usort($this->rules, function(Rule $rule, Rule $rule2) {
if ($rule->priority == $rule2->priority) {
return 0;
}
return ($rule->priority > $rule2->priority) ? 1 : -1;
});
foreach ($this->rules as $rule) {
$rule->apply();
}
}
/**
* Returns the family name if used as a string
*
* @return string
*/
public function __toString() {
return $this->name;
}
}
@@ -0,0 +1,30 @@
<?php
namespace AIOWPS\Firewall;
trait File_Prefix_Trait {
/**
* Get the file's prefix content. N.B. Some code assumes that this doesn't change, so review all consumers of this method before changing its output.
*
* @return string
*/
public static function get_file_content_prefix() {
$prefix = "<?php __halt_compiler();\n";
$prefix .= "/**\n";
$prefix .= " * This file was created by All In One Security (AIOS) plugin.\n";
$prefix .= self::get_prefix_description();
$prefix .= " */\n";
return $prefix;
}
/**
* Returns the description of the file
* You can override this method for each file that needs a file prefix in order to give it its own description
*
* @return string
*/
public static function get_prefix_description() {
return " * The file is required for storing and retrieving your firewall's settings.\n";
}
}
@@ -0,0 +1,84 @@
<?php
namespace AIOWPS\Firewall;
class Allow_List {
/**
* Include a file prefix when the file is created
*/
use File_Prefix_Trait;
/**
* Holds the path to the allow list
*
* @var string
*/
private static $path;
/**
* Overwrite the prefix description from File_Prefix_Trait
*
* @return string
*/
public static function get_prefix_description() {
return " * The file is required for storing and retrieving your firewall's allow list.\n";
}
/**
* Checks whether the user's IP address is in the allow list
*
* @return bool
*/
public static function is_ip_allowed() {
$ips = self::get_ips();
if (empty($ips)) return false;
$ips = explode("\n", $ips);
return \AIOS_Helper::is_user_ip_address_within_list($ips);
}
/**
* Returns the list of IP addresses in the allow list
*
* @return string
*/
public static function get_ips() {
clearstatcache();
if (!file_exists(self::$path)) return '';
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Cannot use WP API. Firewall is loaded independent of WP.
$contents = file_get_contents(self::$path, false, null, strlen(self::get_file_content_prefix()));
return (false !== $contents ? trim($contents) : '');
}
/**
* Set the path of the allow list
*
* @param string $path
* @return void
*/
public static function set_path($path) {
self::$path = $path;
}
/**
* Add IPs to the allow list
* This overwrites the whole allow list with the given IPs
*
* @param mixed $ips - A string of IPs; one per line or an array of individual IPs
* @return bool
*/
public static function add_ips($ips) {
if (is_array($ips)) $ips = implode("\n", $ips);
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Cannot use WP API. Firewall is loaded independent of WP.
return (false !== file_put_contents(self::$path, self::get_file_content_prefix().$ips));
}
}
@@ -0,0 +1,163 @@
<?php
// phpcs:disable WordPress.WP.AlternativeFunctions -- WP isn't loaded here. WP API is unavailable
namespace AIOWPS\Firewall;
/**
* Gives us access to our firewall's config
*/
class Config {
use File_Prefix_Trait;
/**
* The path to our config file
*
* @var string
*/
protected $path;
/**
* Constructs object
*
* @param string $path
*/
public function __construct($path) {
$this->path = $path;
$this->init_file();
}
/**
* Initialise the file if it doesn't exist
*
* @return void
*/
private function init_file() {
clearstatcache();
if (!file_exists($this->path)) {
$dir = dirname($this->path);
if (!file_exists($dir)) Utility::wp_mkdir_p($dir);
file_put_contents($this->path, self::get_file_content_prefix() . json_encode(array()));
}
}
/**
* Update the config file with the new prefix whenever the prefix changes.
*
* @return void
*/
public function update_prefix() {
$valid_prefix = self::get_file_content_prefix();
$current_prefix = file_get_contents($this->path, false, null, 0, strlen($valid_prefix));
if ($current_prefix === $valid_prefix) return; // prefix is valid
$contents = file_get_contents($this->path);
$matches = array();
if (preg_match('/\{.*\}/', $contents, $matches)) {
//update settings
file_put_contents($this->path, $valid_prefix . $matches[0]);
} else {
//reset settings
file_put_contents($this->path, $valid_prefix . json_encode(array()));
}
}
/**
* Gets the value from the config array
*
* @param string $key
* @return mixed|null
*/
public function get_value($key) {
$contents = $this->get_contents();
if (null === $contents) {
return null;
}
if (!isset($contents[$key])) {
return null;
}
return $contents[$key];
}
/**
* Sets a value in our config array
*
* @param string $key
* @param mixed $value
* @return boolean
*/
public function set_value($key, $value) {
$contents = $this->get_contents();
if (null === $contents) {
return false;
}
$contents[$key] = $value;
return (false !== file_put_contents($this->path, self::get_file_content_prefix() . json_encode($contents), LOCK_EX));
}
/**
* Loads the config array from file
*
* @return string
*/
public function get_contents() {
clearstatcache();
if (!file_exists($this->path)) $this->init_file();
// __COMPILER_HALT_OFFSET__ doesn't define in a few PHP versions. It's a PHP bug.
// https://bugs.php.net/bug.php?id=70164
$contents = file_get_contents($this->path, false, null, strlen(self::get_file_content_prefix()));
if (false === $contents) {
return null;
}
if (empty($contents)) {
return array();
}
return json_decode($contents, true);
}
/**
* Sets entire firewall config from array.
*
* @param Array $contents
*
* @return Boolean
*/
public function set_contents($contents) {
if (null === $contents) {
return false;
}
return (false !== file_put_contents($this->path, self::get_file_content_prefix() . json_encode($contents), LOCK_EX));
}
/**
* Returns the path
*
* @return string
*/
public function __toString() {
return $this->path;
}
}
// phpcs:enable WordPress.WP.AlternativeFunctions -- WP isn't loaded here. WP API is unavailable
@@ -0,0 +1,213 @@
<?php
namespace AIOWPS\Firewall;
/**
* A class for accessing constants (including from wp-config) from the firewall
* Only supports parsing 'defines' that have scalar types: int, float, boolean, string and null
*/
class Constants implements \ArrayAccess, \IteratorAggregate {
/**
* The list of constants parsed
*
* @var array
*/
protected $constants;
/**
* The token part of the token identifier
*
* @see https://www.php.net/manual/en/function.token-get-all#refsect1-function.token-get-all-returnvalues
*/
const TOKEN = 0;
/**
* The string content of the token identifier
*
* @see https://www.php.net/manual/en/function.token-get-all#refsect1-function.token-get-all-returnvalues
*/
const CONTENT = 1;
/**
* The line number of the token identifier
*
* @see https://www.php.net/manual/en/function.token-get-all#refsect1-function.token-get-all-returnvalues
*/
const LINE = 2;
/**
* Offset for define's name [ define(NAME, VALUE); ]
*/
const DEFINE_NAME_OFFSET = 2;
/**
* Offset for define's value [ define(NAME, VALUE); ]
*/
const DEFINE_VALUE_OFFSET = 4;
/**
* Constructs our object
*/
public function __construct() {
$this->constants = array();
$this->populate_constants();
}
/**
* Populates our internal constant array with the defines from wp-config
*
* @return void
*/
protected function populate_constants() {
$wpconfig = Utility::get_wpconfig_path();
clearstatcache();
if (!file_exists($wpconfig)) return;
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- WP isn't loaded. WP_Filesystem cannot be used.
$source = file_get_contents($wpconfig);
if (false === $source) return;
$tokens = token_get_all($source);
//Filter out any unwanted tokens
$tokens = array_values(array_filter($tokens, function($token) {
//All tokens that are not arrays are allowed
if (!is_array($token)) return true;
$unwanted_tokens = array(
'T_COMMENT',
'T_WHITESPACE',
'T_DOC_COMMENT',
);
return (!in_array(token_name($token[self::TOKEN]), $unwanted_tokens));
}));
$token_count = count($tokens);
for ($i = 0; $i < $token_count; $i++) {
$current = $tokens[$i];
if (!is_array($current)) continue;
if ('T_STRING' === token_name($current[self::TOKEN]) && 'define' === strtolower($current[self::CONTENT])) {
// Name of the define without the surrounding quotes
$name = substr($tokens[$i + self::DEFINE_NAME_OFFSET][self::CONTENT], 1, -1);
// Grabs the value of the define
$value = $tokens[$i + self::DEFINE_VALUE_OFFSET];
if (!is_array($value)) continue;
// We need to interpret the data type of the define's value
switch (token_name($value[self::TOKEN])) {
case 'T_CONSTANT_ENCAPSED_STRING':
$this->constants[$name] = substr($value[self::CONTENT], 1, -1);
break;
case 'T_LNUMBER':
$this->constants[$name] = intval($value[self::CONTENT]);
break;
case 'T_DNUMBER':
$this->constants[$name] = floatval($value[self::CONTENT]);
break;
case 'T_STRING':
$this->constants[$name] = filter_var($value[self::CONTENT], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
break;
default:
continue 2;
}
}
}
}
/**
* Access the constants as properties
*
* @param string $name
* @return mixed
*/
public function __get($name) {
return $this[$name];
}
/**
* Iterate over the constants
*
* @return iterable
*/
#[\ReturnTypeWillChange]
public function getIterator() {
foreach ($this->constants as $name => $value) yield $name => $value;
foreach (get_defined_constants() as $name => $value) yield $name => $value;
}
/**
* Gives us array access to the constants
*
* @param mixed $offset
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset) {
if (defined($offset)) {
return constant($offset);
} elseif (isset($this->constants[$offset])) {
return $this->constants[$offset];
} else {
return null;
}
}
/**
* Checks if the constant exists
*
* @param mixed $offset
* @return boolean
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset) {
return defined($offset) || isset($this->constants[$offset]);
}
/**
* Check if constant exists
*
* @param string $name
* @return boolean
*/
public function __isset($name) {
return $this->offsetExists($name);
}
/**
* Sets the constant. This is disabled as we want it read-only
*
* @param mixed $offset
* @param mixed $value
* @return void
* @throws \Exception - Throws an exception if called to ensure it's read-only.
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Needed for ArrayAccess interface but not used by us as we require read-only
throw new \Exception('Constants are read-only.');
}
/**
* Unsets the constant. This is disabled as we want it read-only
*
* @param mixed $offset
* @return void
* @throws \Exception - Throws an exception if called to ensure it's read-only.
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Needed for ArrayAccess interface but not used by us as we require read-only
throw new \Exception('Constants are read-only.');
}
}
@@ -0,0 +1,59 @@
<?php
namespace AIOWPS\Firewall;
/**
* Class to help debug the firewall
*/
class Debug {
/**
* Constructs our object
*/
public function __construct() {
//Capture the events that relate to the firewall's rules
Event::capture('rule_triggered', array($this, 'rule_debug'));
Event::capture('rule_not_triggered', array($this, 'rule_debug'));
Event::capture('rule_active', array($this, 'rule_debug'));
Event::capture('rule_not_active', array($this, 'rule_debug'));
}
/**
* Captures the firewall's events for debugging rules
*
* @global Constants $aiowps_firewall_constants
* @global Message_Store $aiowps_firewall_message_store
*
* @param string $event
* @param Rule $rule
*
* @return void
*/
public function rule_debug($event, Rule $rule) {
global $aiowps_firewall_constants, $aiowps_firewall_message_store;
if (!$aiowps_firewall_constants->AIOS_FIREWALL_DEBUG && 'rule_triggered' !== $event) return;
$details = array(
'name' => $rule->name,
'family' => $rule->family,
'ip' => \AIOS_Helper::get_user_ip_address(),
'time' => time(),
);
// Get any user information
foreach ($_COOKIE as $key => $value) {
if (preg_match('/^wordpress_logged_in_/', $key)) {
$details['potential_user'] = stripslashes($value);
break;
}
}
$details['request'] = $_SERVER;
unset($details['request']['HTTP_COOKIE']);
// Uncomment when the firewall log issues have been resolved
//$aiowps_firewall_message_store->set($event, $details);
// Remove when the firewall log issues have been resolved
$aiowps_firewall_message_store->clear_message_store();
}
}
@@ -0,0 +1,51 @@
<?php
namespace AIOWPS\Firewall;
class Event {
/**
* Stores our events
*
* @var array
*/
private static $events = array();
/**
* Captures an event
*
* @param string $name - Name of the event
* @param callable $callback - Callback to execute when the event is raised
* @return void
*/
public static function capture($name, callable $callback) {
$name = strtolower($name);
if (!isset(self::$events[$name])) {
self::$events[$name] = array();
}
self::$events[$name][] = $callback;
}
/**
* Raises the event
*
* All the callbacks in a given name are executed
*
* @param string $name - Name of the event to raise
* @param array ...$args - Variable list of arguments to pass to the callback
* @return void
*/
public static function raise($name, ...$args) {
$name = strtolower($name);
if (empty(self::$events[$name])) return;
array_unshift($args, $name);
foreach (self::$events[$name] as $event) {
call_user_func_array($event, $args);
}
}
}
@@ -0,0 +1,8 @@
<?php
namespace AIOWPS\Firewall;
/**
* Use this when throwing an exception if you want to also exit the request
*/
class Exit_Exception extends \Exception {
}
@@ -0,0 +1,209 @@
<?php
namespace AIOWPS\Firewall;
class Message_Store {
/**
* Makes this class a singleton
*/
use Singleton_Trait;
/**
* Internal store of the messages
*
* @var array
*/
private $messages;
/**
* Holds the name of the message store's table
*
* @var string
*/
private $table_name;
/**
* A key should only be loaded from the database once per request; this keeps track of them
*
* @var array
*/
private $keys_loaded;
/**
* Constructs our object
*/
private function __construct() {
Event::capture('action_before_exit', array($this, 'dump'));
$this->messages = array();
$this->keys_loaded = array();
$this->table_name = 'aiowps_message_store';
}
/**
* Sets internal message store
*
* @param string $key
* @param mixed $value
* @return void
*/
public function set($key, $value) {
if (!is_string($key)) return;
if (!isset($this->messages[$key])) {
$this->messages[$key] = array();
}
$this->messages[$key][] = $value;
}
/**
* Gets the messages associated with a key
*
* @param string $key
* @return array
*/
public function get($key) {
$is_key_loaded = in_array($key, $this->keys_loaded);
$can_check_database = isset($GLOBALS['wpdb']) && !$is_key_loaded && class_exists('Updraft_Semaphore_3_0');
//Load requested messages from the database
if ($can_check_database) {
$lock = new \Updraft_Semaphore_3_0('aios_message_store_lock_'.$key, 60);
$to_delete = array();
if ($lock->lock()) {
try {
global $wpdb;
$table = $this->get_table();
// If we can't get the table to check the DB, still check our internal store for the key
if (empty($table)) return isset($this->messages[$key]) ? $this->messages[$key] : array();
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Ignore.
$rows = $wpdb->get_results($wpdb->prepare("SELECT id, message_value FROM `{$table}` WHERE message_key = %s", $key));
if (!empty($rows)) {
foreach ($rows as $row) {
$values = json_decode($row->message_value, true);
foreach ($values as $value) $this->set($key, $value);
$to_delete[] = $row->id;
}
$this->keys_loaded[] = $key;
}
} catch (\Exception $e) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- PCP warning. Necessary for AIOS error reporting system.
error_log("AIOS: Error getting database entries for key '{$key}': {$e->getMessage()}");
} 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
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- PCP warning. Necessary for AIOS error reporting system.
error_log("AIOS: Error getting database entries for key '{$key}': {$e->getMessage()}");
} finally {
//Delete IDs of loaded messages
if (!empty($to_delete)) {
$ids = implode(',', $to_delete);
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Ignore.
$wpdb->query("DELETE FROM `{$table}` WHERE id IN ({$ids})");
}
$lock->release();
}
}
}
return isset($this->messages[$key]) ? $this->messages[$key] : array();
}
/**
* Dumps the message store to the database
*
* @return void
*/
public function dump() {
//No point saving if there are no messages
if (empty($this->messages)) return;
if (!Utility::attempt_to_access_wpdb()) throw new Exit_Exception('Unable to save the message store to the database: wpdb is inaccessible.');
global $wpdb;
$table = $this->get_table();
if (empty($table)) throw new Exit_Exception('Unable to save messages store to the database: unable to get the correct table.');
$statement = "INSERT INTO `{$table}` (message_key, message_value, created) VALUES ";
$values = array();
foreach ($this->messages as $key => $value) {
$statement .= '(%s, %s, %s),';
$values[] = $table;
$values[] = $key;
$values[] = json_encode($value); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- This method runs outside the WordPress environment and therefore cannot use WordPress functions.
$values[] = time();
}
$statement = rtrim($statement, ',');
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Prepared above.
$wpdb->query($wpdb->prepare($statement, $values));
}
/**
* Returns the table name if it exists
*
* @return string - Table name on success; blank string otherwise
*/
private function get_table() {
global $wpdb;
if (!$wpdb) return '';
$table = $wpdb->get_blog_prefix(0).$this->table_name;
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Ignore.
if ($table != $wpdb->get_var("SHOW TABLES LIKE '{$table}'")) return '';
return $table;
}
/**
* Clears all the messages from the message store table if it contains data.
*
* @return void
*/
public function clear_message_store() {
global $wpdb;
$table = $this->get_table();
// Check if the table exists and is accessible
if (empty($table)) {
return;
}
//Check if the table has any rows
// phpcs:ignore WordPress.DB.DirectDatabaseQuery -- PCP warning. Direct query necessary. No caching necessary.
$row_exists = $wpdb->get_var(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- PCP error. Ignore.
$wpdb->prepare("SELECT EXISTS (SELECT 1 FROM `{$table}` LIMIT 1)")
);
// If there are no rows, $row_exists will be 0
if (!$row_exists) {
return;
}
// Clear the table (delete all records)
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- PCP error. Ignore.
$wpdb->query($wpdb->prepare("DELETE FROM `{$table}`"));
}
}
@@ -0,0 +1,36 @@
<?php
namespace AIOWPS\Firewall;
/**
* A trait with a basic singleton implementation
*/
trait Singleton_Trait {
/**
* Internally stores the class's instance
*
* @var object
*/
private static $instance = null;
/**
* Returns an instance of the class
*
* @return object
*/
public static function instance() {
if (is_null(self::$instance)) self::$instance = new self();
return self::$instance;
}
/**
* We don't want our singleton object to be cloned
*
* @return void
*/
private function __clone() {
}
}
@@ -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;
}
}
@@ -0,0 +1,104 @@
<?php
namespace AIOWPS\Firewall;
/**
* The firewall can be loaded from several different contexts. This class detects from which context the firewall is loaded.
*/
class Context {
/**
* Possible contexts where the firewall can be loaded
*/
const DIRECTIVE = 'directive';
const PLUGINS_LOADED = 'plugins_loaded';
const WP_CONFIG = 'wp-config';
const MU_PLUGIN = 'mu-plugin';
/**
* Get the current context where the firewall is running
*
* @return string
*/
public static function current() {
$incs = get_included_files();
$index = self::get_bootstrap_index($incs);
$is_setup = (-1 !== $index);
if (!$is_setup) return self::PLUGINS_LOADED;
if (0 === $index) return self::DIRECTIVE;
if (preg_match('/wp-config\.php$/i', $incs[$index-1])) {
return self::WP_CONFIG;
}
if (preg_match('/aios-firewall-loader\.php$/', $incs[$index-1])) {
return self::MU_PLUGIN;
}
return self::DIRECTIVE;
}
/**
* Check if we're in a context safe to run WordPress functions
*
* @return boolean
*/
public static function wordpress_safe() {
return (self::plugins_loaded() || self::mu_plugin());
}
/**
* Check if the current context is `plugins_loaded`
*
* @return boolean
*/
public static function plugins_loaded() {
return (self::PLUGINS_LOADED === self::current());
}
/**
* Check if the current context is `directive` (i.e: auto_prepend_file)
*
* @return boolean
*/
public static function directive() {
return (self::DIRECTIVE === self::current());
}
/**
* Check if the current context is `wp_config`
*
* @return boolean
*/
public static function wp_config() {
return (self::WP_CONFIG === self::current());
}
/**
* Check if the current context is `mu_plugin`
*
* @return boolean
*/
public static function mu_plugin() {
return (self::MU_PLUGIN === self::current());
}
/**
* Locate the bootstrap file's index
*
* @param array $incs
* @return int
*/
private static function get_bootstrap_index(array $incs) {
foreach ($incs as $index => $file) {
if (preg_match('/aios-bootstrap\.php$/', $file)) {
return $index;
}
}
return -1;
}
}
@@ -0,0 +1,214 @@
<?php
namespace AIOWPS\Firewall;
if (!defined('AIOWPS_FIREWALL_DIR')) {
header('HTTP/1.1 403 Forbidden');
exit();
}
/**
* Loads and executes our firewall
*/
class Loader {
/**
* Reference to itself
*
* @var Loader
*/
protected static $instance = null;
/**
* Loads and builds all the necessary files
*
* @return void
*/
public function load_firewall() {
try {
/**
* The preloader file should not be directly accessed.
* It should only be loaded via the bootstrap file or in a WordPress context
*/
if ($this->is_preloader_directly_accessed()) return;
$this->init();
global $aiowps_firewall_constants;
if ($aiowps_firewall_constants->AIOS_NO_FIREWALL) return;
//Allow list for bypassing PHP rules
if (Allow_List::is_ip_allowed()) return;
$families = new Family_Collection(Family_Builder::get_families());
foreach (Rule_Builder::get_active_rule() as $rule) {
$families->add_rule_to_member($rule);
}
foreach ($families->get_family() as $member) {
$member->apply_all();
}
} catch (Exit_Exception $e) {
$this->log_message($e->getMessage());
exit();
} catch (\Exception $e) {
$this->log_message($e->getMessage());
} 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
$this->log_message($e->getMessage());
}
}
/**
* Performs general initialisation
*
* @return void
*/
public function init() {
$this->init_includes();
$this->init_services();
new Debug();
}
/**
* Detects whether the preloader file (wp-security-firewall.php) was directly accessed
*
* @return boolean
*/
public function is_preloader_directly_accessed() {
return (1 === preg_match('/wp-security-firewall\.php$/', get_included_files()[0]));
}
/**
* Log our error messages
*
* @param string $message
* @return void
*/
private function log_message($message) {
if (function_exists('do_action')) {
do_action('aios_firewall_loader_log_error', $message, $this);
}
error_log('AIOS firewall error: ' . $message);
}
/**
* Initialises our services
*
* @return void
*/
private function init_services() {
$workspace = $this->get_firewall_workspace();
if (empty($workspace)) {
throw new \Exception('unable to locate workspace.');
}
$GLOBALS['aiowps_firewall_config'] = new Config($workspace . 'settings.php');
$GLOBALS['aiowps_firewall_message_store'] = Message_Store::instance();
$GLOBALS['aiowps_firewall_constants'] = new Constants();
Allow_List::set_path($workspace.'allowlist.php');
}
/**
* Get our workspace directory, i.e., where we save and load data to and from.
*
* @return string
*/
private function get_firewall_workspace() {
global $aiowps_firewall_rules_path;
$workspace = '';
if (!empty($aiowps_firewall_rules_path)) {
$workspace = $aiowps_firewall_rules_path;
} elseif (Context::wordpress_safe()) {
$workspace = \AIOWPSecurity_Utility_Firewall::get_firewall_rules_path();
}
return $workspace;
}
/**
* Registers the autoloader
*
* @return void
*/
private function init_includes() {
spl_autoload_register(function($class) {
if (0 === strpos($class, "AIOWPS\\Firewall\\")) { //only autoload the firewall's files
$relative_classname = substr($class, strlen("AIOWPS\\Firewall\\"), strlen($class)-1);
$classname = str_replace('_', '-', strtolower($relative_classname));
$file = "wp-security-firewall-{$classname}.php";
$rule = "{$classname}.php";
$paths = array(
AIOWPS_FIREWALL_DIR."/{$file}",
AIOWPS_FIREWALL_DIR."/family/{$file}",
AIOWPS_FIREWALL_DIR."/rule/{$file}",
AIOWPS_FIREWALL_DIR."/rule/actions/{$classname}.php",
AIOWPS_FIREWALL_DIR."/rule/rules/{$rule}",
AIOWPS_FIREWALL_DIR."/rule/rules/6g/{$rule}",
AIOWPS_FIREWALL_DIR."/rule/rules/bruteforce/{$rule}",
AIOWPS_FIREWALL_DIR."/rule/rules/blacklist/{$rule}",
AIOWPS_FIREWALL_DIR."/rule/rules/general/{$rule}",
AIOWPS_FIREWALL_DIR."/rule/rules/bots/{$rule}",
AIOWPS_FIREWALL_DIR."/libs/{$file}",
AIOWPS_FIREWALL_DIR."/libs/traits/{$classname}.php",
);
clearstatcache();
foreach ($paths as $path) {
if (file_exists($path)) {
include_once($path);
break;
}
}
}
});
// Manually include needed files
$classes_dir = dirname(AIOWPS_FIREWALL_DIR);
$manual_files = array(
$classes_dir.'/wp-security-firewall-resource-unavailable.php',
$classes_dir.'/wp-security-firewall-resource.php',
$classes_dir.'/wp-security-helper.php',
);
foreach ($manual_files as $file) {
clearstatcache();
if (file_exists($file)) include_once $file;
}
if (Context::wordpress_safe()) {
include_once("{$classes_dir}/wp-security-utility-file.php");
}
}
/**
* Gets or creates an instance of this object
*
* @return Loader
*/
public static function get_instance() {
if (null === self::$instance) {
return new self();
}
return self::$instance;
}
}
@@ -0,0 +1,218 @@
<?php
namespace AIOWPS\Firewall;
class Utility {
/**
* Returns the directory of where the WordPress files are installed
* This differs from get_root_dir() when WordPress is setup in a subdirectory
*
* @return string
*/
public static function get_wordpress_dir() {
if (Context::wordpress_safe()) {
return wp_normalize_path(ABSPATH);
}
global $aiowps_firewall_data;
return isset($aiowps_firewall_data['ABSPATH']) ? $aiowps_firewall_data['ABSPATH'] : '';
}
/**
* Returns the root directory of the site
* This may be different from where the WordPress files are installed if WordPress is setup in a subdirectory
*
* @return string|null
*/
public static function get_root_dir() {
if (Context::wordpress_safe()) {
return \AIOWPSecurity_Utility_File::get_home_path();
}
// We're in the firewall context here, so get the root directory from the bootstrap file path
$includes = get_included_files();
foreach ($includes as $file) {
if (preg_match('/aios-bootstrap\.php$/', $file)) {
return self::normalize_path(dirname($file).'/');
}
}
return null;
}
/**
* Normalizes the file path
*
* @see https://developer.wordpress.org/reference/functions/wp_normalize_path/
* @param string $path
* @return string
*/
public static function normalize_path($path) {
// Standardize all paths to use '/'.
$path = str_replace('\\', '/', $path);
// Replace multiple slashes down to a singular, allowing for network shares having two slashes.
$path = preg_replace('|(?<=.)/+|', '/', $path);
// Windows paths should uppercase the drive letter.
if (':' === substr($path, 1, 1)) {
$path = ucfirst($path);
}
return $path;
}
/**
* Returns the path to wp-config.php
*
* @param string $root - Where to look for wp-config.php file
* @return string
*/
public static function get_wpconfig_path($root = '') {
if (empty($root)) $root = self::get_wordpress_dir();
$wp_config_file = $root . 'wp-config.php';
if (file_exists($wp_config_file)) {
return $wp_config_file;
} elseif (file_exists(dirname($root) . '/wp-config.php')) {
return dirname($root) . '/wp-config.php';
}
return $wp_config_file;
}
/**
* Recursive directory creation based on full path
*
* @see https://developer.wordpress.org/reference/functions/wp_mkdir_p/
* @param string $target
* @return bool
*/
public static function wp_mkdir_p($target) {
$wrapper = null;
// Strip the protocol.
if (self::wp_is_stream($target)) {
list($wrapper, $target) = explode('://', $target, 2);
}
// From php.net/mkdir user contributed notes.
$target = str_replace('//', '/', $target);
// Put the wrapper back on the target.
if (null !== $wrapper) {
$target = $wrapper . '://' . $target;
}
/*
* Safe mode fails with a trailing slash under certain PHP versions.
* Use rtrim() instead of untrailingslashit to avoid formatting.php dependency.
*/
$target = rtrim($target, '/');
if (empty($target)) {
$target = '/';
}
if (file_exists($target)) {
return @is_dir($target);
}
// Do not allow path traversals.
if (false !== strpos($target, '../') || false !== strpos($target, '..' . DIRECTORY_SEPARATOR)) {
return false;
}
// We need to find the permissions of the parent folder that exists and inherit that.
$target_parent = dirname($target);
while ('.' !== $target_parent && ! is_dir($target_parent) && dirname($target_parent) !== $target_parent) {
$target_parent = dirname($target_parent);
}
// Get the permission bits
$stat = @stat($target_parent);
if ($stat) {
$dir_perms = $stat['mode'] & 0007777;
} else {
$dir_perms = 0777;
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- PCP error. WP not loaded. WP API not available.
if (@mkdir($target, $dir_perms, true)) {
/*
* If a umask is set that modifies $dir_perms, we'll have to re-set
* the $dir_perms correctly with chmod()
*/
if (($dir_perms & ~umask()) != $dir_perms) {
$folder_parts = explode('/', substr($target, strlen($target_parent) + 1));
for ($i = 1, $c = count($folder_parts); $i <= $c; $i++) {
// phpcs:ignore WordPress.WP.AlternativeFunctions -- PCP error. WP not loaded. WP API not available.
chmod($target_parent . '/' . implode('/', array_slice($folder_parts, 0, $i)), $dir_perms);
}
}
return true;
}
return false;
}
/**
* Tests if a given path is a stream URL
*
* @see https://developer.wordpress.org/reference/functions/wp_is_stream/
* @param string $path
* @return bool
*/
public static function wp_is_stream($path) {
$scheme_separator = strpos($path, '://');
if (false === $scheme_separator) {
// $path isn't a stream.
return false;
}
$stream = substr($path, 0, $scheme_separator);
return in_array($stream, stream_get_wrappers(), true);
}
/**
* Attempts to give us access to the $wpdb object from the firewall.
* This should only be used when you're sure WordPress will not be loading after the firewall.
*
* @return bool
*/
public static function attempt_to_access_wpdb() {
// wpdb is already accessible
if (isset($GLOBALS['wpdb'])) return true;
$wp_path = self::get_wordpress_dir() . 'wp-load.php';
clearstatcache();
if (!file_exists($wp_path)) return false;
define('SHORTINIT', true);
$included = (bool) include $wp_path;
global $wpdb;
// If $wpdb is inaccessible by this point, it means loading wp-settings didn't complete.
// So we have to manually include the wp-config (which includes wp-settings) for it to complete.
if (empty($wpdb) && $included) include self::get_wpconfig_path();
global $wpdb;
return !empty($wpdb);
}
}
@@ -0,0 +1,21 @@
<?php
/**
* This is the file that will be loaded using auto_prepend_file directive
*/
if (!defined('AIOWPS_FIREWALL_DIR')) {
define('AIOWPS_FIREWALL_DIR', dirname(__FILE__));
define('AIOWPS_FIREWALL_DIR_PARENT', dirname(AIOWPS_FIREWALL_DIR));
}
if (!defined('AIOWPSEC_FIREWALL_DONE')) {
//Gracefully handle if the file is unable to be included. (i.e: ensure the user's site does not crash)
if (!(@include_once AIOWPS_FIREWALL_DIR . '/wp-security-firewall-loader.php')) {
error_log('AIOS firewall error: failed to load the firewall. Unable to include wp-security-firewall-loader.php.');
return;
}
\AIOWPS\Firewall\Loader::get_instance()->load_firewall();
define('AIOWPSEC_FIREWALL_DONE', true);
}