File "class-waf-runtime.php"
Full Path: /home/warrior1/public_html/languages/wp-content/plugins/jetpack/jetpack_vendor/automattic/jetpack-waf/src/class-waf-runtime.php
File size: 20.45 KB
MIME-type: text/x-php
Charset: utf-8
<?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 ) );
}
}