File "Processor.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/vendor-prod/patchstack/firewall/src/Processor.php
File size: 17.33 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* @license GPL-3.0-or-later
*
* Modified using Strauss.
* @see https://github.com/BrianHenryIE/strauss
*/
namespace iThemesSecurity\Strauss\Patchstack;
use iThemesSecurity\Strauss\Patchstack\Response;
use iThemesSecurity\Strauss\Patchstack\Request;
use iThemesSecurity\Strauss\Patchstack\Extensions\ExtensionInterface;
class Processor
{
/**
* The firewall rules to process.
*
* @var array
*/
private $firewallRules = [];
/**
* The whitelist rules to process.
*
* @var array
*/
private $whitelistRules = [];
/**
* The options of the engine.
*
* @var array
*/
private $options = [
'autoblockAttempts' => 10,
'autoblockMinutes' => 30,
'autoblockTime' => 60,
'whitelistKeysRules' => [],
'mustUsePluginCall' => false
];
/**
* The extension that will process specific logic for the CMS.
*
* @var ExtensionInterface
*/
private $extension;
/**
* The captured request that needs to be inspected.
*
* @var Request
*/
private $request;
/**
* The response that will be sent, depending on the action executed by the processor.
*
* @var Response
*/
private $response;
/**
* Creates a new processor instance.
*
* @param ExtensionInterface $extension
* @param array $firewallRules
* @param array $whitelistRules
* @param array $options
* @return void
*/
public function __construct(
ExtensionInterface $extension,
$firewallRules = [],
$whitelistRules = [],
$options = []
) {
$this->extension = $extension;
$this->firewallRules = $firewallRules;
$this->whitelistRules = $whitelistRules;
$this->options = array_merge($this->options, $options);
$this->request = new Request($this->options, $this->extension);
$this->response = new Response($this->options);
}
/**
* Magic getter for the options.
*
* @param string $name
* @return mixed
*/
public function __get($name)
{
return isset($this->options[$name]) ? $this->options[$name] : null;
}
/**
* Launch the firewall. First we determine if the user is blocked and whitelisted, then go through
* all of the firewall rules.
*
* Will return true if $mustExit is false and all of the rules were processed without a positive detection.
*
* @param boolean $mustExit
* @return boolean
*/
public function launch($mustExit = true)
{
// Determine if the user is temporarily blocked from the site before we do anything else.
$isWhitelisted = $this->extension->canBypass($this->mustUsePluginCall);
if (!$isWhitelisted && $this->extension->isBlocked($this->autoblockMinutes, $this->autoblockTime, $this->autoblockAttempts)) {
$this->extension->forceExit(22);
}
// Determine if the firewall and whitelist rules were parsed properly.
if (!is_array($this->firewallRules) || !is_array($this->whitelistRules)) {
return true;
}
// Determine if we have any firewall and/or whitelist rules loaded.
if (count($this->firewallRules) == 0 && count($this->whitelistRules) == 0) {
return true;
}
// Merge the rules together. First iterate through the whitelist rules because
// we want to whitelist the request if there's a whitelist rule match.
$rules = array_merge($this->whitelistRules, $this->firewallRules);
// Iterate through all the firewall rules.
foreach ($rules as $rule) {
// Should never happen.
if (!isset($rule['rules']) || empty($rule['rules'])) {
continue;
}
// If this rule should respect the whitelist, we check this before we continue.
if (isset($rule['bypass_whitelist']) && ($rule['bypass_whitelist'] === 0 || $rule['bypass_whitelist'] === false) && $isWhitelisted) {
continue;
}
// If the rule contains matching type we cannot call during mu-plugins, skip.
$hasWpAction = $this->hasWpAction($rule['rules']);
if (defined('PS_FW_MU_RAN') && !$hasWpAction || $this->mustUsePluginCall && $hasWpAction) {
continue;
}
// Execute the firewall rule.
$rule_hit = $this->executeFirewall($rule['rules']);
// If the payload did not match the rule, continue on to the next rule.
if (!$rule_hit) {
continue;
}
// Capture the POST data for logging purposes.
if ($rule['type'] != 'WHITELIST') {
$postData = $this->request->getParameterValues('log');
}
// Determine what action to perform.
if ($rule['type'] == 'BLOCK') {
$this->extension->logRequest($rule['id'], $postData, 'BLOCK');
// Do we have to exit the page or simply return false?
if ($mustExit) {
$this->extension->forceExit($rule['id']);
} else {
return false;
}
} elseif ($rule['type'] == 'LOG') {
$this->extension->logRequest($rule['id'], $postData, 'LOG');
} elseif ($rule['type'] == 'REDIRECT') {
$this->extension->logRequest($rule['id'], $postData, 'REDIRECT');
$this->response->redirect($rule['type_params'], $mustExit);
} elseif ($rule['type'] == 'WHITELIST') {
return $mustExit;
}
}
return true;
}
/**
* Execute the firewall rules.
*
* @param array $rules
* @return bool
*/
public function executeFirewall($rules)
{
// Count number of inclusive rules, if any.
$inclusiveCount = 0;
if (count($rules) > 1) {
$inclusiveCount = $this->getInclusiveCount($rules);
}
// Keep track of how many inclusive rule hits.
$inclusiveHits = 0;
// Loop through all of the conditions for this rule.
foreach ($rules as $rule) {
// Parameter must always be present.
if (!isset($rule['parameter'])) {
continue;
}
// Cast to an array so we can iterate through all parameters.
if (!is_array($rule['parameter'])) {
$parameters = [$rule['parameter']];
} else {
$parameters = $rule['parameter'];
}
// Iterate through all parameters.
foreach ($parameters as $parameter) {
// Extract the value of the paramater that we want.
$values = $this->request->getParameterValues($parameter);
if (is_null($values) && $parameter !== false && $parameter != 'rules') {
continue;
}
// For special parameter values we just set the array to a single null value.
if ($parameter === false || $parameter == 'rules') {
$values = [null];
}
// For all field matches, we want to execute the rule against it.
foreach ($values as $value) {
// Apply mutations, if any.
if (isset($rule['mutations']) && is_array($rule['mutations'])) {
$value = $this->request->applyMutation($rule['mutations'], $value);
if (is_null($value)) {
continue;
}
}
// Perform the matching.
if (isset($rule['match']) && is_array($rule['match']) || isset($rule['rules'])) {
// Do we have to process child-rules?
if (isset($rule['rules'])) {
$match = $this->executeFirewall($rule['rules']);
} else {
$match = $this->matchParameterValue($rule['match'], $value);
}
// Is the rule a match?
if ($match) {
// In case there are multiple rules, they may require chained AND conditions.
if ($inclusiveCount <= 1 || !isset($rule['inclusive']) || $rule['inclusive'] !== true) {
return true;
} else {
$inclusiveHits++;
break 2;
}
}
}
}
}
}
// In case we hit all of the AND conditions.
if ($inclusiveCount > 1 && $inclusiveHits >= $inclusiveCount) {
return true;
}
return false;
}
/**
* Get the number of inclusive rules as part of the rule group.
*
* @param array $rules
* @return int
*/
public function getInclusiveCount($rules)
{
if (count($rules) == 1) {
return 1;
}
$count = 0;
foreach ($rules as $rule) {
if (isset($rule['inclusive']) && $rule['inclusive'] === true) {
$count++;
}
}
return $count;
}
/**
* With the given parameter value, attempt to match it.
*
* @param mixed $match
* @param mixed $value
* @return bool
*/
public function matchParameterValue($match, $value)
{
// Take some of the parameters for easy access.
$matchType = isset($match['type']) ? $match['type'] : null;
$matchValue = isset($match['value']) ? $match['value'] : null;
// Perform a match depending on the given match type.
// If a scalar matches another scalar (loose).
if ($matchType == 'equals' && is_scalar($value) && is_scalar($matchValue)) {
return $matchValue == $value;
}
// If a scalar matches another scaler (strict).
if ($matchType == 'equals_strict' && is_scalar($value) && is_scalar($matchValue)) {
return $matchValue === $value;
}
// If a scalar is bigger than another scalar.
if ($matchType == 'more_than' && is_scalar($value) && is_scalar($matchValue)) {
return $value > $matchValue;
}
// If a scalar is less than another scalar.
if ($matchType == 'less_than' && is_scalar($value) && is_scalar($matchValue)) {
return $value < $matchValue;
}
// If the parameter is present at all.
if ($matchType == 'isset') {
return true;
}
// If a scalar is a ctype alnum with underscores, dashes and spaces.
if ($matchType == 'ctype_special' && is_scalar($value) && $value != '') {
$value = str_replace([' ', '_', '-', ','], '', $value);
$isClean = (bool) (@preg_match('/^[\w$\x{0080}-\x{FFFF}]*$/u', $value) > 0);
return $isClean === $matchValue;
}
// If a scaler is a ctype digit.
if ($matchType == 'ctype_digit' && is_scalar($value) && $value != '') {
return @ctype_digit($value) === $matchValue;
}
// If a scaler is a ctype alnum.
if ($matchType == 'ctype_alnum' && is_scalar($value) && $value != '') {
$isClean = (bool) (@preg_match('/^[\w$\x{0080}-\x{FFFF}]*$/u', $value) > 0);
return $isClean === $matchValue;
}
// If a scalar is numeric.
if ($matchType == 'is_numeric' && is_scalar($value) && $value != '') {
return @is_numeric($value) === $matchValue;
}
// If a scalar contains a value.
if (($matchType == 'contains' || $matchType == 'stripos') && is_scalar($value)) {
return @stripos($value, $matchValue) !== false;
}
// If a scalar does not contain a value.
if ($matchType == 'not_contains' && is_scalar($value)) {
return @stripos($value, $matchValue) === false;
}
// If a scalar contains single or double quotes.
if (($matchType == 'quotes' || $matchType == 'inline_js_xss') && is_scalar($value)) {
return @stripos($value, '"') !== false || @stripos($value, "'") !== false;
}
// If a string matches a regular expression.
if ($matchType == 'regex' && is_string($matchValue) && is_scalar($value)) {
return @preg_match($matchValue, @urldecode($value)) === 1;
}
// If the user does not have a WP privilege.
if ($matchType == 'current_user_cannot' && is_scalar($matchValue) && function_exists('current_user_can') && function_exists('wp_get_current_user') && !$this->mustUsePluginCall) {
return @!current_user_can($matchValue);
}
// If a value is in an array.
if ($matchType == 'in_array' && !is_array($value) && is_array($matchValue)) {
return @in_array($value, $matchValue);
}
// If a value is not in an array.
if ($matchType == 'not_in_array' && !is_array($value) && is_array($matchValue)) {
return @!in_array($value, $matchValue);
}
// If an array of values is in another array of values.
if ($matchType == 'array_in_array' && is_array($value) && is_array($matchValue)) {
return @count(@array_intersect($value, $matchValue)) > 0;
}
// If a specific parameter key matches a sub-match condition.
if ($matchType == 'array_key_value' && isset($match['key'], $match['match'])) {
// To support arrays for the key matching type value.
$keys = is_array($match['key']) ? $match['key'] : [$match['key']];
// Iterate through all keys.
foreach ($keys as $key) {
$values = $this->request->getParameterValues($key, $value);
if (!is_array($values)) {
continue;
}
foreach ($values as $val) {
if ($this->matchParameterValue($match['match'], $val)) {
return true;
}
}
}
return false;
}
// If the user provided value does not match the current hostname.
if ($matchType == 'hostname' && is_string($value)) {
if (empty($value)) {
return false;
}
// If there's no protocol we add it.
if (substr($value, 0, 4) != 'http') {
$value = 'https://' . $value;
}
// We only care about the hostname.
$host = @parse_url($value, PHP_URL_HOST);
if (!$host) {
return true;
}
return $host !== $this->extension->getHostName();
}
// If any of the uploaded files in the parameter matches a sub-match condition.
if ($matchType == 'file_contains' && isset($match['match'])) {
// Extract all tmp_names.
if (isset($value['tmp_name'])) {
$files = $value['tmp_name'];
if (!is_array($files)) {
$files = [$files];
}
} else {
$files = array_column($value, 'tmp_name');
}
// No need to continue if there are no files.
if (is_array($files) && count($files) === 0) {
return false;
}
// Cast all tmp_names to a single-dimension array.
$files = $this->request->getArrayValues($files, '', 'array');
if (is_array($files) && count($files) === 0) {
return false;
}
// Get the contents of the files.
$contents = '';
foreach ($files as $file) {
$contents .= (string) @file_get_contents($file);
}
// Now attempt to match it.
return $this->matchParameterValue($match['match'], $contents);
}
// If a scalar passes a run through wp_kses_post.
if ($matchType == 'general_xss' && is_scalar($value) && function_exists('wp_kses_post')) {
return $value != @\wp_kses_post($value);
}
// If a scalar passes a run through inline_js_xss.
if ($matchType == 'inline_xss' && is_scalar($value)) {
if (@stripos($value, '"') === false && @stripos($value, "'") === false) {
return false;
}
if (@stripos($value, '>') !== false || @stripos($value, '=') !== false) {
return true;
}
return false;
}
return false;
}
/**
* Determine if the rules contain an action that should not be executed under the mu-plugins context.
*
* @param array $rules
* @return boolean
*/
private function hasWpAction($rules)
{
$functions = ['current_user_cannot'];
if (isset($rules['rules'])) {
if ($this->hasWpAction($rules['rules'])) {
return true;
}
}
foreach ($rules as $rule) {
if (!isset($rule['match'], $rule['match']['type'])) {
continue;
}
if (in_array($rule['match']['type'], $functions)) {
return true;
}
}
return false;
}
}