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-runner.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * Entrypoint for actually executing the WAF. * * @package automattic/jetpack-waf */ namespace Automattic\Jetpack\Waf; use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Modules; use Jetpack_Options; /** * Executes the WAF. */ class Waf_Runner { const WAF_RULES_VERSION = '1.0.0'; const MODE_OPTION_NAME = 'jetpack_waf_mode'; const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list'; const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list'; const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list'; const RULES_FILE = __DIR__ . '/../rules/rules.php'; const ALLOW_IP_FILE = __DIR__ . '/../rules/allow-ip.php'; const BLOCK_IP_FILE = __DIR__ . '/../rules/block-ip.php'; const VERSION_OPTION_NAME = 'jetpack_waf_rules_version'; const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp'; const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data'; /** * Run the WAF */ public static function initialize() { if ( ! self::is_enabled() ) { return; } self::define_mode(); self::define_share_data(); if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) { return; } // Don't run if in standalone mode if ( function_exists( 'add_action' ) ) { self::add_hooks(); } if ( ! self::did_run() ) { self::run(); } } /** * Set action hooks * * @return void */ public static function add_hooks() { add_action( 'update_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'activate' ), 10, 0 ); add_action( 'update_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'activate' ), 10, 0 ); add_action( 'update_option_' . self::IP_LISTS_ENABLED_OPTION_NAME, array( static::class, 'activate' ), 10, 0 ); add_action( 'jetpack_waf_rules_update_cron', array( static::class, 'update_rules_cron' ) ); // TODO: This doesn't exactly fit here - may need to find another home if ( ! wp_next_scheduled( 'jetpack_waf_rules_update_cron' ) ) { wp_schedule_event( time(), 'twicedaily', 'jetpack_waf_rules_update_cron' ); } // Register REST routes. add_action( 'rest_api_init', array( new REST_Controller(), 'register_rest_routes' ) ); } /** * Set the mode definition if it has not been set. * * @return void */ public static function define_mode() { if ( ! defined( 'JETPACK_WAF_MODE' ) ) { $mode_option = get_option( self::MODE_OPTION_NAME ); define( 'JETPACK_WAF_MODE', $mode_option ); } } /** * Set the mode definition if it has not been set. * * @return void */ public static function define_share_data() { if ( ! defined( 'JETPACK_WAF_SHARE_DATA' ) ) { $share_data_option = get_option( self::SHARE_DATA_OPTION_NAME, false ); define( 'JETPACK_WAF_SHARE_DATA', $share_data_option ); } } /** * Did the WAF run yet or not? * * @return bool */ public static function did_run() { return defined( 'JETPACK_WAF_RUN' ); } /** * Determines if the passed $option is one of the allowed WAF operation modes. * * @param string $option The mode option. * @return bool */ public static function is_allowed_mode( $option ) { // Normal constants are defined prior to WP_CLI running causing problems for activation if ( defined( 'WAF_CLI_MODE' ) ) { $option = WAF_CLI_MODE; } $allowed_modes = array( 'normal', 'silent', ); return in_array( $option, $allowed_modes, true ); } /** * Determines if the WAF module is enabled on the site. * * @return bool */ public static function is_enabled() { // if ABSPATH is defined, then WordPress has already been instantiated, // so we can check to see if the waf module is activated. if ( defined( 'ABSPATH' ) ) { return ( new Modules() )->is_active( 'waf' ); } return true; } /** * Runs the WAF and potentially stops the request if a problem is found. * * @return void */ public static function run() { // Make double-sure we are only running once. if ( self::did_run() ) { return; } Waf_Constants::initialize_constants(); // if ABSPATH is defined, then WordPress has already been instantiated, // and we're running as a plugin (meh). Otherwise, we're running via something // like PHP's prepend_file setting (yay!). define( 'JETPACK_WAF_RUN', defined( 'ABSPATH' ) ? 'plugin' : 'preload' ); // if the WAF is being run before a command line script, don't try to execute rules (there's no request). if ( PHP_SAPI === 'cli' ) { return; } // if something terrible happens during the WAF running, we don't want to interfere with the rest of the site, // so we intercept errors ONLY while the WAF is running, then we remove our handler after the WAF finishes. $display_errors = ini_get( 'display_errors' ); // phpcs:ignore ini_set( 'display_errors', 'Off' ); // phpcs:ignore set_error_handler( array( self::class, 'errorHandler' ) ); try { // phpcs:ignore $waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() ); // execute waf rules. // phpcs:ignore include self::RULES_FILE; } catch ( \Exception $err ) { // phpcs:ignore // Intentionally doing nothing. } // remove the custom error handler, so we don't interfere with the site. restore_error_handler(); // phpcs:ignore ini_set( 'display_errors', $display_errors ); } /** * Error handler to be used while the WAF is being executed. * * @param int $code The error code. * @param string $message The error message. * @param string $file File with the error. * @param string $line Line of the error. * @return void */ public static function errorHandler( $code, $message, $file, $line ) { // phpcs:ignore // Intentionally doing nothing for now. } /** * Initializes the WP filesystem. * * @return void * @throws \Exception If filesystem is unavailable. */ public static function initialize_filesystem() { if ( ! function_exists( '\\WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if ( ! \WP_Filesystem() ) { throw new \Exception( 'No filesystem available.' ); } } /** * Activates the WAF by generating the rules script and setting the version * * @return void */ public static function activate() { self::define_mode(); if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) { return; } $version = get_option( self::VERSION_OPTION_NAME ); if ( ! $version ) { add_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION ); } add_option( self::SHARE_DATA_OPTION_NAME, true ); self::initialize_filesystem(); self::create_waf_directory(); self::generate_ip_rules(); self::create_blocklog_table(); self::generate_rules(); } /** * Created the waf directory on activation. * * @return void * @throws \Exception In case there's a problem when creating the directory. */ public static function create_waf_directory() { WP_Filesystem(); Waf_Constants::initialize_constants(); global $wp_filesystem; if ( ! $wp_filesystem ) { throw new \Exception( 'Can not work without the file system being initialized.' ); } if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) { if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) { throw new \Exception( 'Failed creating WAF standalone bootstrap file directory: ' . JETPACK_WAF_DIR ); } } } /** * Create the log table when plugin is activated. * * @return void */ public static function create_blocklog_table() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $sql = " CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog ( log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, timestamp datetime NOT NULL, rule_id BIGINT NOT NULL, reason longtext NOT NULL, PRIMARY KEY (log_id), KEY timestamp (timestamp) ) "; dbDelta( $sql ); } /** * Deactivates the WAF by deleting the relevant options and emptying rules file. * * @return void * @throws \Exception If file writing fails. */ public static function deactivate() { delete_option( self::MODE_OPTION_NAME ); delete_option( self::VERSION_OPTION_NAME ); global $wp_filesystem; self::initialize_filesystem(); if ( ! $wp_filesystem->put_contents( self::RULES_FILE, "<?php\n" ) ) { throw new \Exception( 'Failed to empty rules.php file.' ); } } /** * Tries periodically to update the rules using our API. * * @return void */ public static function update_rules_cron() { self::define_mode(); if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) { return; } self::generate_ip_rules(); self::generate_rules(); update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() ); } /** * Updates the rule set if rules version has changed * * @return void */ public static function update_rules_if_changed() { self::define_mode(); if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) { return; } $version = get_option( self::VERSION_OPTION_NAME ); if ( self::WAF_RULES_VERSION !== $version ) { update_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION ); self::generate_ip_rules(); self::generate_rules(); } } /** * Handle updates to the WAF */ public static function update_waf() { self::update_rules_if_changed(); // Re-generate the standalone bootstrap file on every update // TODO: We may consider only doing this when the WAF version changes ( new Waf_Standalone_Bootstrap() )->generate(); } /** * Retrieve rules from the API * * @throws \Exception If site is not registered. * @throws \Exception If API did not respond 200. * @throws \Exception If data is missing from response. * @return array */ public static function get_rules_from_api() { $blog_id = Jetpack_Options::get_option( 'id' ); if ( ! $blog_id ) { throw new \Exception( 'Site is not registered' ); } $response = Client::wpcom_json_api_request_as_blog( sprintf( '/sites/%s/waf-rules', $blog_id ), '2', array(), null, 'wpcom' ); $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $response_code ) { throw new \Exception( 'API connection failed.', $response_code ); } $rules_json = wp_remote_retrieve_body( $response ); $rules = json_decode( $rules_json, true ); if ( empty( $rules['data'] ) ) { throw new \Exception( 'Data missing from response.' ); } return $rules['data']; } /** * Generates the rules.php script * * @throws \Exception If file writing fails. * @return void */ public static function generate_rules() { /** * WordPress filesystem abstraction. * * @var \WP_Filesystem_Base $wp_filesystem */ global $wp_filesystem; self::initialize_filesystem(); $api_exception = null; $throw_api_exception = true; try { $rules = self::get_rules_from_api(); } catch ( \Exception $e ) { if ( 401 === $e->getCode() ) { // do not throw API exceptions for users who do not have access $throw_api_exception = false; } if ( $wp_filesystem->exists( self::RULES_FILE ) && $throw_api_exception ) { throw $e; } $rules = "<?php\n"; $api_exception = $e; } // Ensure that the folder exists. if ( ! $wp_filesystem->is_dir( dirname( self::RULES_FILE ) ) ) { $wp_filesystem->mkdir( dirname( self::RULES_FILE ) ); } $ip_allow_rules = self::ALLOW_IP_FILE; $ip_block_rules = self::BLOCK_IP_FILE; $ip_list_code = "if ( require('$ip_allow_rules') ) { return; }\n" . "if ( require('$ip_block_rules') ) { return \$waf->block('block', -1, 'ip block list'); }\n"; $rules_divided_by_line = explode( "\n", $rules ); array_splice( $rules_divided_by_line, 1, 0, $ip_list_code ); $rules = implode( "\n", $rules_divided_by_line ); if ( ! $wp_filesystem->put_contents( self::RULES_FILE, $rules ) ) { throw new \Exception( 'Failed writing rules file to: ' . self::RULES_FILE ); } if ( null !== $api_exception && $throw_api_exception ) { throw $api_exception; } } /** * We allow for both, one IP per line or comma-; semicolon; or whitespace-separated lists. This also validates the IP addresses * and only returns the ones that look valid. * * @param string $ips List of ips - example: "8.8.8.8\n4.4.4.4,2.2.2.2;1.1.1.1 9.9.9.9,5555.5555.5555.5555". * @return array List of valid IP addresses. - example based on input example: array('8.8.8.8', '4.4.4.4', '2.2.2.2', '1.1.1.1', '9.9.9.9') */ private static function ip_option_to_array( $ips ) { $ips = (string) $ips; $ips = preg_split( '/[\s,;]/', $ips ); $result = array(); foreach ( $ips as $ip ) { if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) { $result[] = $ip; } } return $result; } /** * Generates the rules.php script * * @throws \Exception If filesystem is not available. * @throws \Exception If file writing fails. * @return void */ public static function generate_ip_rules() { /** * WordPress filesystem abstraction. * * @var \WP_Filesystem_Base $wp_filesystem */ global $wp_filesystem; self::initialize_filesystem(); // Ensure that the folder exists. if ( ! $wp_filesystem->is_dir( dirname( self::RULES_FILE ) ) ) { $wp_filesystem->mkdir( dirname( self::RULES_FILE ) ); } $allow_list = self::ip_option_to_array( get_option( self::IP_ALLOW_LIST_OPTION_NAME ) ); $block_list = self::ip_option_to_array( get_option( self::IP_BLOCK_LIST_OPTION_NAME ) ); $lists_enabled = (bool) get_option( self::IP_LISTS_ENABLED_OPTION_NAME ); if ( false === $lists_enabled ) { // Making the lists empty effectively disabled the feature while still keeping the other WAF rules evaluation active. $allow_list = array(); $block_list = array(); } $allow_rules_content = ''; // phpcs:disable WordPress.PHP.DevelopmentFunctions $allow_rules_content .= '$waf_allow_list = ' . var_export( $allow_list, true ) . ";\n"; // phpcs:enable $allow_rules_content .= 'return $waf->is_ip_in_array( $waf_allow_list );' . "\n"; if ( ! $wp_filesystem->put_contents( self::ALLOW_IP_FILE, "<?php\n$allow_rules_content" ) ) { throw new \Exception( 'Failed writing allow list file to: ' . self::ALLOW_IP_FILE ); } $block_rules_content = ''; // phpcs:disable WordPress.PHP.DevelopmentFunctions $block_rules_content .= '$waf_block_list = ' . var_export( $block_list, true ) . ";\n"; // phpcs:enable $block_rules_content .= 'return $waf->is_ip_in_array( $waf_block_list );' . "\n"; if ( ! $wp_filesystem->put_contents( self::BLOCK_IP_FILE, "<?php\n$block_rules_content" ) ) { throw new \Exception( 'Failed writing block list file to: ' . self::BLOCK_IP_FILE ); } } }