File "ValueResolver.php"
Full Path: /home/lacostenacom/public_html/wp/wp./wp-content/plugins/imunify-security/inc/App/Defender/ValueResolver.php
File size: 13.76 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Copyright (с) Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
*
* Licensed under CLOUD LINUX LICENSE AGREEMENT
* https://www.cloudlinux.com/legal/
*/
namespace CloudLinux\Imunify\App\Defender;
use CloudLinux\Imunify\App\Defender\Model\Condition;
use CloudLinux\Imunify\App\Defender\Model\ConditionSource;
/**
* Resolves candidate values from a Request based on a parsed condition name.
*
* Handles the three resolution modes (field regex, scan-all, single field),
* bracket-path navigation, URI decoding, and source dispatching.
*
* @since 3.0.0
*/
class ValueResolver {
/**
* Resolve candidate values for a condition from the request.
*
* Returns a mixed[] of values (strings or arrays) that should each be
* tested by the caller's matcher. The caller is responsible for type
* checking and leaf extraction on array values.
*
* @param Condition $condition The condition to resolve values for.
* @param Request $request Request object.
*
* @return array Candidate values to test.
*/
public function resolveValues( Condition $condition, Request $request ) {
if ( ! $condition->hasRequiredFields() ) {
return array();
}
$parsed = $condition->parseName();
$source = $parsed['source'];
$values = array();
if ( null !== $parsed['field_regex'] ) {
$regexValues = $this->getFieldValuesByRegex( $request, $source, $parsed['field_regex'] );
if ( null !== $parsed['bracket_path'] ) {
$values = $this->navigateBracketPathIntoValues( array_values( $regexValues ), $parsed['bracket_path'] );
} else {
$values = array_values( $regexValues );
}
} elseif ( null === $parsed['field'] && $this->isCollectionSource( $source ) ) {
$values = array_values( $this->getAllSourceValues( $request, $source ) );
} elseif ( ConditionSource::REQUEST_URI === $source ) {
$values = array( $this->getDecodedUri( $request ) );
} elseif ( null !== $parsed['bracket_path'] && self::bracketPathHasRegex( $parsed['bracket_path'] ) ) {
$values = $this->resolveFieldWithRegexBrackets( $request, $parsed );
} else {
$value = $this->getFieldValue( $request, $parsed );
if ( null !== $value ) {
$values = array( $value );
}
}
// ARGS_NAMES is a field-less source — always returns all key names regardless of parsed field.
if ( ConditionSource::ARGS_NAMES === $source ) {
return $request->getArgNames();
}
if ( ConditionSource::ARGS === $source && ! empty( $values ) ) {
return self::decodeArgValues( $values );
}
return $values;
}
/**
* Get field value from request based on parsed condition name.
*
* Supports bracket-notation for nested PHP arrays (e.g., ARGS:param[key]).
* Resolution order: nested array traversal first, literal key fallback.
*
* @param Request $request Request object.
* @param array $parsed Parsed condition name from Condition::parseNameString().
*
* @return string|array<string, mixed>|null Field value or null if not found.
*/
public function getFieldValue( $request, $parsed ) {
$source = $parsed['source'];
$field = $parsed['field'];
$bracketPath = isset( $parsed['bracket_path'] ) ? $parsed['bracket_path'] : null;
$rawField = isset( $parsed['raw_field'] ) ? $parsed['raw_field'] : null;
switch ( $source ) {
case ConditionSource::ARGS:
if ( null === $field ) {
return null;
}
if ( null !== $bracketPath ) {
$value = $request->resolveNestedGet( $field, $bracketPath );
if ( null === $value ) {
$value = $request->resolveNestedPost( $field, $bracketPath );
}
if ( null !== $value ) {
return $value;
}
$value = $request->get( $rawField );
if ( null === $value ) {
$value = $request->post( $rawField );
}
return $value;
}
$fieldValue = $request->get( $field );
if ( null === $fieldValue ) {
$fieldValue = $request->post( $field );
}
return $fieldValue;
case ConditionSource::REQUEST_URI:
return $this->getDecodedUri( $request );
case ConditionSource::FILES:
if ( null === $field ) {
return null;
}
$filesParsed = self::parseFilesField( $field );
if ( null !== $filesParsed['sub'] ) {
return self::getFilesSubValue( $request, $filesParsed['field'], $filesParsed['sub'] );
}
return $request->getFile( $field );
case ConditionSource::REQUEST_COOKIES:
if ( null === $field ) {
return null;
}
return $request->cookie( $field );
case ConditionSource::REQUEST_HEADERS:
if ( null === $field ) {
return null;
}
return $request->getHeader( $field );
default:
return null;
}
}
/**
* Parse FILES sub-selector from field name.
*
* FILES:async-upload:name => field=async-upload, sub=name
* FILES:file:type => field=file, sub=type
* FILES:file:content => field=file, sub=content
* FILES:file => field=file, sub=null (legacy exists-only)
*
* @since 3.0.2
*
* @param string $field The field string (part after first colon in condition name).
*
* @return array Array with 'field' and 'sub' keys.
*/
public static function parseFilesField( $field ) {
$segments = explode( ':', $field, 2 );
if ( 2 === count( $segments ) ) {
return array(
'field' => $segments[0],
'sub' => $segments[1],
);
}
return array(
'field' => $field,
'sub' => null,
);
}
/**
* Get a FILES sub-value (name, filename, type, or content).
*
* @since 3.0.2
*
* @param Request $request Request object.
* @param string $field The file field key (e.g. 'async-upload').
* @param string $sub The sub-selector (name, filename, type, content).
*
* @return string|null The sub-value or null if not available.
*/
public static function getFilesSubValue( $request, $field, $sub ) {
switch ( $sub ) {
case 'name':
case 'filename':
return $request->getFileName( $field );
case 'type':
return $request->getFileType( $field );
case 'content':
return $request->getFileContent( $field );
default:
return null;
}
}
/**
* Get all field values from request whose names match a regex pattern.
*
* @param Request $request Request object.
* @param string $source Field source (e.g., ARGS, REQUEST_COOKIES).
* @param string $fieldRegex Regex pattern for field names (without delimiters).
*
* @return array<string, mixed> Associative array of matching field name => value pairs.
*/
private function getFieldValuesByRegex( $request, $source, $fieldRegex ) {
switch ( $source ) {
case ConditionSource::ARGS:
return $request->getMatchingArgs( $fieldRegex );
case ConditionSource::REQUEST_COOKIES:
return $request->getMatchingCookies( $fieldRegex );
case ConditionSource::REQUEST_HEADERS:
return $request->getMatchingHeaders( $fieldRegex );
default:
return array();
}
}
/**
* Check whether a source represents a collection of named values.
*
* Collection sources (ARGS, REQUEST_COOKIES, REQUEST_HEADERS) contain
* multiple named fields and support scan-all semantics when no specific
* field is given.
*
* @param string $source Condition source constant.
*
* @return bool True if the source is a collection.
*/
private function isCollectionSource( $source ) {
return in_array(
$source,
array( ConditionSource::ARGS, ConditionSource::REQUEST_COOKIES, ConditionSource::REQUEST_HEADERS ),
true
);
}
/**
* Get all values for a source (scan-all mode).
*
* @param Request $request Request object.
* @param string $source Condition source constant.
*
* @return array Associative array of field name => value pairs.
*/
private function getAllSourceValues( $request, $source ) {
switch ( $source ) {
case ConditionSource::ARGS:
return $request->getAllArgs();
case ConditionSource::REQUEST_COOKIES:
return $request->getAllCookies();
case ConditionSource::REQUEST_HEADERS:
return $request->getAllHeaders();
default:
return array();
}
}
/**
* Check whether a bracket path contains any regex segments.
*
* @since 3.0.0
*
* @param array $bracketPath Array of bracket-path segments.
*
* @return bool True if at least one segment is a /regex/ pattern.
*/
private static function bracketPathHasRegex( array $bracketPath ) {
foreach ( $bracketPath as $segment ) {
if ( preg_match( '#^/(.+)/$#', $segment ) ) {
return true;
}
}
return false;
}
/**
* Resolve a literal field value then navigate regex-aware bracket path.
*
* Used when the field name is literal but bracket segments contain /regex/.
*
* @since 3.0.0
*
* @param Request $request Request object.
* @param array $parsed Parsed condition name from Condition::parseNameString().
*
* @return array Resolved leaf values.
*/
private function resolveFieldWithRegexBrackets( Request $request, array $parsed ) {
$source = $parsed['source'];
$field = $parsed['field'];
if ( null === $field ) {
return array();
}
$rootValue = null;
switch ( $source ) {
case ConditionSource::ARGS:
$rootValue = $request->get( $field );
if ( null === $rootValue ) {
$rootValue = $request->post( $field );
}
break;
case ConditionSource::REQUEST_COOKIES:
$rootValue = $request->cookie( $field );
break;
case ConditionSource::REQUEST_HEADERS:
$rootValue = $request->getHeader( $field );
break;
default:
return array();
}
if ( null === $rootValue ) {
return array();
}
return self::navigateBracketPath( $rootValue, $parsed['bracket_path'] );
}
/**
* Navigate a bracket path into each value from a regex-matched set.
*
* Each regex-matched value is expected to be an array. The bracket path
* segments are traversed into each value. Segments wrapped in /regex/
* are treated as regex patterns that match multiple keys at that level.
*
* @since 3.0.0
*
* @param array $values Flat array of matched values.
* @param array $bracketPath Array of bracket-path segments.
*
* @return array Resolved leaf values after bracket navigation.
*/
private function navigateBracketPathIntoValues( array $values, array $bracketPath ) {
$results = array();
foreach ( $values as $value ) {
$navigated = self::navigateBracketPath( $value, $bracketPath );
foreach ( $navigated as $leaf ) {
$results[] = $leaf;
}
}
return $results;
}
/**
* Navigate a single value through bracket-path segments.
*
* Literal segments perform a direct array key lookup. Segments matching
* the /regex/ convention iterate over keys at that level.
*
* @since 3.0.0
*
* @param mixed $value The value to navigate into.
* @param array $bracketPath Array of bracket-path segments.
*
* @return array Resolved leaf values (may contain strings or arrays).
*/
private static function navigateBracketPath( $value, array $bracketPath ) {
$current = array( $value );
foreach ( $bracketPath as $segment ) {
$next = array();
if ( preg_match( '#^/(.+)/$#', $segment, $m ) ) {
$regex = '#^(?:' . str_replace( '#', '\\#', $m[1] ) . ')$#';
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- invalid regex silently skipped.
if ( false === @preg_match( $regex, '' ) ) {
return array();
}
foreach ( $current as $item ) {
if ( ! is_array( $item ) ) {
continue;
}
foreach ( $item as $key => $val ) {
if ( preg_match( $regex, (string) $key ) ) {
$next[] = $val;
}
}
}
} else {
foreach ( $current as $item ) {
if ( is_array( $item ) && isset( $item[ $segment ] ) ) {
$next[] = $item[ $segment ];
}
}
}
if ( empty( $next ) ) {
return array();
}
$current = $next;
}
return $current;
}
/**
* Apply an extra URL-decode pass to resolved ARGS values.
*
* PHP's $_GET/$_POST parsing performs a single urldecode. This method
* applies one additional urldecode to match the double-decode already
* used for REQUEST_URI, closing the double-encoding evasion gap.
*
* @since 3.0.2
*
* @param array $values Resolved ARGS values (strings or nested arrays).
*
* @return array Values with one additional URL-decode applied.
*/
private static function decodeArgValues( array $values ) {
return array_map( array( __CLASS__, 'decodeArgValue' ), $values );
}
/**
* URL-decode a single ARGS value, recursing into arrays.
*
* Non-string scalars (int, float, bool) are cast to their string form so
* the downstream match operators (matchEquals / matchContains / matchRegex
* / detectXSS / detectSQLi) see a consistent string type. Without this
* cast, JSON-bodied REST requests such as `{"user_id": 1, "role": 5}`
* would produce PHP-int leaves that Request::extractLeafValues()
* silently drops (the walker collects only is_string() values), so a rule
* like `detectSQLi on ARGS:role` would evaluate to false regardless of
* payload content. null is coerced to empty string for the same reason.
*
* @since 3.0.2
*
* @param mixed $value The value to decode.
*
* @return mixed Decoded value (string, array, or unchanged object).
*/
private static function decodeArgValue( $value ) {
if ( is_string( $value ) ) {
return urldecode( $value );
}
if ( is_array( $value ) ) {
return array_map( array( __CLASS__, 'decodeArgValue' ), $value );
}
if ( is_bool( $value ) ) {
return $value ? '1' : '0';
}
if ( null === $value ) {
return '';
}
if ( is_int( $value ) || is_float( $value ) ) {
return (string) $value;
}
return $value;
}
/**
* Get the decoded request URI for condition evaluation.
*
* Applies urldecode() twice to defend against both single-encoded
* and double-encoded URI bypass attempts (e.g., %2F and %252F).
*
* @param Request $request Request object.
*
* @return string The decoded URI.
*/
private function getDecodedUri( $request ) {
return urldecode( urldecode( $request->getUri() ) );
}
}