Phase 6: AIOS security plugin with conservative login lockdown config (10 attempts)
This commit is contained in:
+13
@@ -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),
|
||||
);
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+30
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
+84
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+163
@@ -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
|
||||
+213
@@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -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();
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+8
@@ -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 {
|
||||
}
|
||||
+209
@@ -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}`"));
|
||||
}
|
||||
}
|
||||
+36
@@ -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() {
|
||||
}
|
||||
}
|
||||
+18
@@ -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();
|
||||
}
|
||||
}
|
||||
+23
@@ -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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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');
|
||||
}
|
||||
}
|
||||
+88
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -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();
|
||||
}
|
||||
}
|
||||
+24
@@ -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");
|
||||
}
|
||||
}
|
||||
+62
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -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',
|
||||
'#/(\$(\&)?|\*|\"|\.|,|&|&?)/?$#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);
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+65
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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.
|
||||
}
|
||||
}
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+98
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+156
@@ -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',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -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']));
|
||||
}
|
||||
|
||||
}
|
||||
+69
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -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']), '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+91
@@ -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;
|
||||
}
|
||||
}
|
||||
+104
@@ -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;
|
||||
}
|
||||
}
|
||||
+214
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+218
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
Executable
+21
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user