Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
overpastor
/
wp
/
wp.
/
wp-content
/
plugins
/
imunify-security
/
inc
/
App
/
Defender
:
ValueResolver.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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() ) ); } }