Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
primogenial
/
wp-content-20241001222009
/
plugins
/
jetpack
/
jetpack_vendor
/
automattic
/
jetpack-waf
/
src
:
class-waf-runtime.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * Runtime for Jetpack Waf * * @package automattic/jetpack-waf */ namespace Automattic\Jetpack\Waf; require_once __DIR__ . '/functions.php'; // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This class is all about sanitizing input. /** * The environment variable that defined the WAF running mode. * * @var string JETPACK_WAF_MODE */ /** * Waf_Runtime class */ class Waf_Runtime { /** * Last rule. * * @var string */ public $last_rule = ''; /** * Matched vars. * * @var array */ public $matched_vars = array(); /** * Matched var. * * @var string */ public $matched_var = ''; /** * Matched var names. * * @var array */ public $matched_var_names = array(); /** * Matched var name. * * @var string */ public $matched_var_name = ''; /** * State. * * @var array */ private $state = array(); /** * Metadata. * * @var array */ private $metadata = array(); /** * Transforms. * * @var Waf_Transforms[] */ private $transforms; /** * Operators. * * @var Waf_Operators[] */ private $operators; /** * Rules to remove. * * @var array[] */ private $rules_to_remove = array( 'id' => array(), 'tag' => array(), ); /** * Targets to remove. * * @var array[] */ private $targets_to_remove = array( 'id' => array(), 'tag' => array(), ); /** * Constructor method. * * @param Waf_Transforms $transforms Transforms. * @param Waf_Operators $operators Operators. */ public function __construct( $transforms, $operators ) { $this->transforms = $transforms; $this->operators = $operators; } /** * Rule removed method. * * @param string $id Ids. * @param string[] $tags Tags. */ public function rule_removed( $id, $tags ) { if ( isset( $this->rules_to_remove['id'][ $id ] ) ) { return true; } foreach ( $tags as $tag ) { if ( isset( $this->rules_to_remove['tag'][ $tag ] ) ) { return true; } } return false; } /** * Update Targets. * * @param array $targets Targets. * @param string $rule_id Rule id. * @param string[] $rule_tags Rule tags. */ public function update_targets( $targets, $rule_id, $rule_tags ) { $updates = array(); // look for target updates based on the rule's ID. if ( isset( $this->targets_to_remove['id'][ $rule_id ] ) ) { foreach ( $this->targets_to_remove['id'][ $rule_id ] as $name => $props ) { $updates[] = array( $name, $props ); } } // look for target updates based on the rule's tags. foreach ( $rule_tags as $tag ) { if ( isset( $this->targets_to_remove['tag'][ $tag ] ) ) { foreach ( $this->targets_to_remove['tag'][ $tag ] as $name => $props ) { $updates[] = array( $name, $props ); } } } // apply any found target updates. foreach ( $updates as list( $name, $props ) ) { if ( isset( $targets[ $name ] ) ) { // we only need to remove targets that exist. if ( true === $props ) { // if the entire target is being removed, remove it. unset( $targets[ $name ] ); } else { // otherwise just mark single props to ignore. $targets[ $name ]['except'] = array_merge( isset( $targets[ $name ]['except'] ) ? $targets[ $name ]['except'] : array(), $props ); } } } return $targets; } /** * Return TRUE if at least one of the targets matches the rule. * * @param string[] $transforms One of the transform methods defined in the Jetpack Waf_Transforms class. * @param mixed $targets Targets. * @param string $match_operator Match operator. * @param mixed $match_value Match value. * @param bool $match_not Match not. * @param bool $capture Capture. * @return bool */ public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) { $this->matched_vars = array(); $this->matched_var_names = array(); $this->matched_var = ''; $this->matched_var_name = ''; $match_found = false; // get values. $values = $this->normalize_targets( $targets ); // apply transforms. foreach ( $transforms as $t ) { foreach ( $values as &$v ) { $v['value'] = $this->transforms->$t( $v['value'] ); } } // pass each target value to the operator to find any that match. $matched = array(); $captures = array(); foreach ( $values as $v ) { $match = $this->operators->{$match_operator}( $v['value'], $match_value ); $did_match = false !== $match; if ( $match_not !== $did_match ) { // If either: // - rule is negated ("not" flag set) and the target was not matched // - rule not negated and the target was matched // then this is considered a match. $match_found = true; $this->matched_var_names[] = $v['source']; $this->matched_vars[] = $v['value']; $this->matched_var_name = end( $this->matched_var_names ); $this->matched_var = end( $this->matched_vars ); $matched[] = array( $v, $match ); // Set any captured matches into state if the rule has the "capture" flag. if ( $capture ) { $captures = is_array( $match ) ? $match : array( $match ); foreach ( array_slice( $captures, 0, 10 ) as $i => $c ) { $this->set_var( "tx.$i", $c ); } } } } return $match_found; } /** * Block. * * @param string $action Action. * @param string $rule_id Rule id. * @param string $reason Block reason. * @param int $status_code Http status code. */ public function block( $action, $rule_id, $reason, $status_code = 403 ) { if ( ! $reason ) { $reason = "rule $rule_id"; } else { $reason = $this->sanitize_output( $reason ); } $this->write_blocklog( $rule_id, $reason ); error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" ); header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" ); if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) { $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP'; header( $protocol . ' 403 Forbidden', true, $status_code ); die( "rule $rule_id - reason $reason" ); } } /** * Write block logs. We won't write to the file if it exceeds 100 mb. * * @param string $rule_id Rule id. * @param string $reason Block reason. */ public function write_blocklog( $rule_id, $reason ) { $log_data = array(); $log_data['rule_id'] = $rule_id; $log_data['reason'] = $reason; $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' ); if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) { $file_path = JETPACK_WAF_DIR . '/waf-blocklog'; $file_exists = file_exists( $file_path ); if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) { $fp = fopen( $file_path, 'a+' ); if ( $fp ) { try { fwrite( $fp, json_encode( $log_data ) . "\n" ); } finally { fclose( $fp ); } } } } $this->write_blocklog_row( $log_data ); } /** * Write block logs to database. * * @param array $log_data Log data. */ private function write_blocklog_row( $log_data ) { $conn = $this->connect_to_wordpress_db(); if ( ! $conn ) { return; } global $table_prefix; $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" ); if ( false !== $statement ) { $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] ); $statement->execute(); if ( $conn->insert_id > 100 ) { $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" ); } } } /** * Connect to WordPress database. */ private function connect_to_wordpress_db() { if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) { return; } require_once JETPACK_WAF_WPCONFIG; $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli if ( $conn->connect_error ) { error_log( 'Could not connect to the database:' . $conn->connect_error ); return null; } return $conn; } /** * Redirect. * * @param string $rule_id Rule id. * @param string $url Url. */ public function redirect( $rule_id, $url ) { error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" ); header( "Location: $url" ); exit; } /** * Flag rule for removal. * * @param string $prop Prop. * @param string $value Value. */ public function flag_rule_for_removal( $prop, $value ) { if ( 'id' === $prop ) { $this->rules_to_remove['id'][ $value ] = true; } else { $this->rules_to_remove['tag'][ $value ] = true; } } /** * Flag target for removal. * * @param string $id_or_tag Id or tag. * @param string $id_or_tag_value Id or tag value. * @param string $name Name. * @param string $prop Prop. */ public function flag_target_for_removal( $id_or_tag, $id_or_tag_value, $name, $prop = null ) { if ( null === $prop ) { $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] = true; } else { if ( ! isset( $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] ) // if the entire target is already being removed then it would be redundant to remove a single property. || true !== $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] ) { $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ][] = $prop; } } } /** * Get variable value. * * @param string $key Key. */ public function get_var( $key ) { return isset( $this->state[ $key ] ) ? $this->state[ $key ] : ''; } /** * Set variable value. * * @param string $key Key. * @param string $value Value. */ public function set_var( $key, $value ) { $this->state[ $key ] = $value; } /** * Increment variable. * * @param string $key Key. * @param mixed $value Value. */ public function inc_var( $key, $value ) { if ( ! isset( $this->state[ $key ] ) ) { $this->state[ $key ] = 0; } $this->state[ $key ] += floatval( $value ); } /** * Decrement variable. * * @param string $key Key. * @param mixed $value Value. */ public function dec_var( $key, $value ) { if ( ! isset( $this->state[ $key ] ) ) { $this->state[ $key ] = 0; } $this->state[ $key ] -= floatval( $value ); } /** * Unset variable. * * @param string $key Key. */ public function unset_var( $key ) { unset( $this->state[ $key ] ); } /** * Meta. * * @param string $key Key. * @param string $prop Prop. */ public function meta( $key, $prop = false ) { if ( ! isset( $this->metadata[ $key ] ) ) { $value = null; switch ( $key ) { case 'headers': $value = array(); foreach ( $_SERVER as $k => $v ) { $k = strtolower( $k ); if ( 'http_' === substr( $k, 0, 5 ) ) { $value[ $this->normalize_header_name( substr( $k, 5 ) ) ] = $v; } elseif ( 'content_type' === $k ) { $value['content-type'] = $v; } elseif ( 'content_length' === $k ) { $value['content-length'] = $v; } } $value['content-type'] = ( ! isset( $value['content-type'] ) || '' === $value['content-type'] ) // default Content-Type per RFC 7231 section 3.1.5.5. ? 'application/octet-stream' : $value['content-type']; $value['content-length'] = ( isset( $value['content-length'] ) && '' !== $value['content-length'] ) ? $value['content-length'] // if the content-length header is missing, default it to zero. : '0'; break; case 'remote_addr': $value = ''; if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { $value = wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ); } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $value = wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ); } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { $value = wp_unslash( $_SERVER['REMOTE_ADDR'] ); } break; case 'request_method': $value = empty( $_SERVER['REQUEST_METHOD'] ) ? 'GET' : wp_unslash( $_SERVER['REQUEST_METHOD'] ); break; case 'request_protocol': $value = empty( $_SERVER['SERVER_PROTOCOL'] ) ? ( empty( $_SERVER['HTTPS'] ) ? 'HTTP' : 'HTTPS' ) : wp_unslash( $_SERVER['SERVER_PROTOCOL'] ); break; case 'request_uri': $value = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; break; case 'request_uri_raw': $value = ( isset( $_SERVER['https'] ) ? 'https://' : 'http://' ) . ( isset( $_SERVER['SERVER_NAME'] ) ? wp_unslash( $_SERVER['SERVER_NAME'] ) : '' ) . $this->meta( 'request_uri' ); break; case 'request_filename': $value = strtok( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '', '?' ); break; case 'request_line': $value = sprintf( '%s %s %s', $this->meta( 'request_method' ), $this->meta( 'request_uri' ), $this->meta( 'request_protocol' ) ); break; case 'request_basename': $value = basename( $this->meta( 'request_filename' ) ); break; case 'request_body': $value = file_get_contents( 'php://input' ); break; case 'query_string': $value = isset( $_SERVER['QUERY_STRING'] ) ? wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; } $this->metadata[ $key ] = $value; } return false === $prop ? $this->metadata[ $key ] : ( isset( $this->metadata[ $key ][ $prop ] ) ? $this->metadata[ $key ][ $prop ] : '' ); } /** * State values. * * @param string $prefix Prefix. */ private function state_values( $prefix ) { $output = array(); $len = strlen( $prefix ); foreach ( $this->state as $k => $v ) { if ( 0 === stripos( $k, $prefix ) ) { $output[ substr( $k, $len ) ] = $v; } } return $output; } /** * Change a string to all lowercase and replace spaces and underscores with dashes. * * @param string $name Name. * @return string */ public function normalize_header_name( $name ) { return str_replace( array( ' ', '_' ), '-', strtolower( $name ) ); } /** * Normalize targets. * * @param array $targets Targets. */ public function normalize_targets( $targets ) { $return = array(); foreach ( $targets as $k => $v ) { $count_only = isset( $v['count'] ); $only = isset( $v['only'] ) ? $v['only'] : array(); $except = isset( $v['except'] ) ? $v['except'] : array(); $_k = strtolower( $k ); switch ( $_k ) { case 'request_headers': $only = array_map( function ( $t ) { return '/' === $t[0] ? $t : $this->normalize_header_name( $t ); }, $only ); $except = array_map( function ( $t ) { return '/' === $t[0] ? $t : $this->normalize_header_name( $t ); }, $except ); $this->normalize_array_target( $this->meta( 'headers' ), $only, $except, $k, $return, $count_only ); continue 2; case 'request_headers_names': $this->normalize_array_target( array_keys( $this->meta( 'headers' ) ), array(), array(), $k, $return, $count_only ); continue 2; case 'request_method': case 'request_protocol': case 'request_uri': case 'request_uri_raw': case 'request_filename': case 'remote_addr': case 'request_basename': case 'request_body': case 'query_string': case 'request_line': $v = $this->meta( $_k ); break; case 'tx': case 'ip': $this->normalize_array_target( $this->state_values( "$k." ), $only, $except, $k, $return, $count_only ); continue 2; case 'request_cookies': $this->normalize_array_target( $_COOKIE, $only, $except, $k, $return, $count_only ); continue 2; case 'request_cookies_names': $this->normalize_array_target( array_keys( $_COOKIE ), array(), array(), $k, $return, $count_only ); continue 2; case 'args': $this->normalize_array_target( $_REQUEST, $only, $except, $k, $return, $count_only ); continue 2; case 'args_names': $this->normalize_array_target( array_keys( $_REQUEST ), array(), array(), $k, $return, $count_only ); continue 2; case 'args_get': $this->normalize_array_target( $_GET, $only, $except, $k, $return, $count_only ); continue 2; case 'args_get_names': $this->normalize_array_target( array_keys( $_GET ), array(), array(), $k, $return, $count_only ); continue 2; case 'args_post': $this->normalize_array_target( $_POST, $only, $except, $k, $return, $count_only ); continue 2; case 'args_post_names': $this->normalize_array_target( array_keys( $_POST ), array(), array(), $k, $return, $count_only ); continue 2; case 'files': $names = array_map( function ( $f ) { return $f['name']; }, $_FILES ); $this->normalize_array_target( $names, $only, $except, $k, $return, $count_only ); continue 2; case 'files_names': $this->normalize_array_target( array_keys( $_FILES ), $only, $except, $k, $return, $count_only ); continue 2; default: var_dump( 'Unknown target', $k, $v ); exit; } $return[] = array( 'name' => $k, 'value' => $v, 'source' => $k, ); } return $return; } /** * Verifies is ip from request is in an array. * * @param array $array Array to verify ip against. */ public function is_ip_in_array( $array ) { $request = new Waf_Request(); $real_ip = $request->get_real_user_ip_address(); return in_array( $real_ip, $array, true ); } /** * Normalize array target. * * @param array $source Source. * @param array $only Only. * @param array $excl Excl. * @param string $name Name. * @param array $results Results. * @param bool $count_only Count only. */ private function normalize_array_target( $source, $only, $excl, $name, &$results, $count_only ) { $output = array(); $has_only = isset( $only[0] ); $has_excl = isset( $excl[0] ); if ( $has_only ) { foreach ( $only as $prop ) { if ( isset( $source[ $prop ] ) && $this->key_matches( $prop, $only ) ) { $output[ $prop ] = $source[ $prop ]; } } } else { $output = $source; } if ( $has_excl ) { foreach ( array_keys( $output ) as $k ) { if ( $this->key_matches( $k, $excl ) ) { unset( $output[ $k ] ); } } } if ( $count_only ) { $results[] = array( 'name' => $name, 'value' => count( $output ), 'source' => '&' . $name, ); } else { foreach ( $output as $tk => $tv ) { if ( is_array( $tv ) ) { // flatten it so we get all the values considered $flat_values = $this->array_flatten( $tv ); foreach ( $flat_values as $fv ) { $results[] = array( // force names to strings // we don't care about the nested keys here, just the overall variable name 'name' => '' . $tk, 'value' => $fv, 'source' => "$name:$tk", ); } } else { $results[] = array( // force names to strings 'name' => '' . $tk, 'value' => $tv, 'source' => "$name:$tk", ); } } } return $results; } /** * Basic array flatten with array_merge; no-op on non-array targets. * * @param array $source Array to flatten. * @return array The flattened array. */ private function array_flatten( $source ) { if ( ! is_array( $source ) ) { return $source; } $return = array(); foreach ( $source as $v ) { if ( is_array( $v ) ) { $return = array_merge( $return, $this->array_flatten( $v ) ); } else { $return[] = $v; } } return $return; } /** * Key matches. * * @param string $input Input. * @param array $patterns Patterns. */ private function key_matches( $input, $patterns ) { foreach ( $patterns as $p ) { if ( '/' === $p[0] ) { if ( 1 === preg_match( $p, $input ) ) { return true; } } else { if ( 0 === strcasecmp( $p, $input ) ) { return true; } } } return false; } /** * Sanitize output generated from the request that was blocked. * * @param string $output Output to sanitize. */ public function sanitize_output( $output ) { $url_decoded_output = rawurldecode( $output ); $html_entities_output = htmlentities( $url_decoded_output, ENT_QUOTES, 'UTF-8' ); // @phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired $escapers = array( "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" ); $replacements = array( "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" ); // @phpcs:enable Squiz.Strings.DoubleQuoteUsage.NotRequired return( str_replace( $escapers, $replacements, $html_entities_output ) ); } }