updated plugin Jetpack Protect version 4.0.0

This commit is contained in:
2025-04-29 21:19:56 +00:00
committed by Gitium
parent eb9181b250
commit ebd40ef928
265 changed files with 11864 additions and 3987 deletions

View File

@ -5,6 +5,78 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.23.8] - 2025-03-24
### Changed
- Internal updates.
## [0.23.7] - 2025-03-17
### Changed
- Internal updates.
## [0.23.6] - 2025-03-12
### Changed
- Internal updates.
## [0.23.5] - 2025-03-10
### Changed
- Ensure check_valid_blocked_user handles error outcomes. [#42036]
## [0.23.4] - 2025-02-24
### Changed
- Update dependencies. [#39263]
## [0.23.3] - 2025-02-03
### Fixed
- Code: Remove extra params on function calls. [#41263]
## [0.23.2] - 2025-01-20
### Changed
- Code: Use function-style exit() and die() with a default status code of 0. [#41167]
## [0.23.1] - 2024-11-25
### Changed
- Updated dependencies. [#40286]
## [0.23.0] - 2024-11-18
### Removed
- General: Update minimum PHP version to 7.2. [#40147]
## [0.22.3] - 2024-11-04
### Added
- Enable test coverage. [#39961]
## [0.22.2] - 2024-10-29
### Changed
- Internal updates. [#39263]
## [0.22.1] - 2024-10-17
### Fixed
- WAF: Improve backwards compatibility for sites running outdated bootstrap scripts via standalone mode. [#39812]
## [0.22.0] - 2024-10-14
### Added
- WAF: Add new properties to the WAF feature's REST API endpoint. [#39511]
### Fixed
- Improve backwards compatibility for sites running in standalone mode. [#39652]
- WAF: Reduce amount of classes autoloaded during standalone mode execution. [#38944]
## [0.21.0] - 2024-10-07
### Added
- Firewall Runtime: Added support for rule files to specify body parser type. [#39516]
## [0.20.1] - 2024-10-01
### Deprecated
- Added back public API as deprecated. [#39606]
## [0.20.0] - 2024-09-30
### Added
- Added Waf_Blocklog_Manager class [#35739]
## [0.19.0] - 2024-09-23
### Added
- Firewall: add support for CIDR ranges in IP lists. [#39425]
## [0.18.5] - 2024-09-06
### Changed
- Updated package dependencies. [#39253]
@ -366,6 +438,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Core: do not ship .phpcs.dir.xml in production builds.
[0.23.8]: https://github.com/Automattic/jetpack-waf/compare/v0.23.7...v0.23.8
[0.23.7]: https://github.com/Automattic/jetpack-waf/compare/v0.23.6...v0.23.7
[0.23.6]: https://github.com/Automattic/jetpack-waf/compare/v0.23.5...v0.23.6
[0.23.5]: https://github.com/Automattic/jetpack-waf/compare/v0.23.4...v0.23.5
[0.23.4]: https://github.com/Automattic/jetpack-waf/compare/v0.23.3...v0.23.4
[0.23.3]: https://github.com/Automattic/jetpack-waf/compare/v0.23.2...v0.23.3
[0.23.2]: https://github.com/Automattic/jetpack-waf/compare/v0.23.1...v0.23.2
[0.23.1]: https://github.com/Automattic/jetpack-waf/compare/v0.23.0...v0.23.1
[0.23.0]: https://github.com/Automattic/jetpack-waf/compare/v0.22.3...v0.23.0
[0.22.3]: https://github.com/Automattic/jetpack-waf/compare/v0.22.2...v0.22.3
[0.22.2]: https://github.com/Automattic/jetpack-waf/compare/v0.22.1...v0.22.2
[0.22.1]: https://github.com/Automattic/jetpack-waf/compare/v0.22.0...v0.22.1
[0.22.0]: https://github.com/Automattic/jetpack-waf/compare/v0.21.0...v0.22.0
[0.21.0]: https://github.com/Automattic/jetpack-waf/compare/v0.20.1...v0.21.0
[0.20.1]: https://github.com/Automattic/jetpack-waf/compare/v0.20.0...v0.20.1
[0.20.0]: https://github.com/Automattic/jetpack-waf/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/Automattic/jetpack-waf/compare/v0.18.5...v0.19.0
[0.18.5]: https://github.com/Automattic/jetpack-waf/compare/v0.18.4...v0.18.5
[0.18.4]: https://github.com/Automattic/jetpack-waf/compare/v0.18.3...v0.18.4
[0.18.3]: https://github.com/Automattic/jetpack-waf/compare/v0.18.2...v0.18.3

View File

@ -4,17 +4,18 @@
"type": "jetpack-library",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.0",
"automattic/jetpack-connection": "^4.0.0",
"automattic/jetpack-constants": "^2.0.4",
"automattic/jetpack-ip": "^0.2.3",
"automattic/jetpack-status": "^4.0.0",
"php": ">=7.2",
"automattic/jetpack-connection": "^6.8.0",
"automattic/jetpack-constants": "^3.0.5",
"automattic/jetpack-ip": "^0.4.6",
"automattic/jetpack-status": "^5.0.10",
"wikimedia/aho-corasick": "^1.0"
},
"require-dev": {
"yoast/phpunit-polyfills": "^1.1.1",
"automattic/jetpack-changelogger": "^4.2.6",
"automattic/wordbless": "@dev"
"yoast/phpunit-polyfills": "^3.0.0",
"automattic/jetpack-changelogger": "^6.0.2",
"automattic/jetpack-test-environment": "@dev",
"automattic/phpunit-select-config": "^1.0.1"
},
"suggest": {
"automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package."
@ -29,14 +30,13 @@
},
"scripts": {
"phpunit": [
"./vendor/phpunit/phpunit/phpunit --configuration tests/php/integration/phpunit.xml.dist --colors=always",
"./vendor/phpunit/phpunit/phpunit --configuration tests/php/unit/phpunit.xml.dist --colors=always"
"phpunit-select-config tests/php/integration/phpunit.#.xml.dist --colors=always",
"phpunit-select-config tests/php/unit/phpunit.#.xml.dist --colors=always"
],
"post-install-cmd": "WorDBless\\Composer\\InstallDropin::copy",
"post-update-cmd": "WorDBless\\Composer\\InstallDropin::copy",
"test-coverage": "tests/action-test-coverage.sh",
"test-coverage-html": [
"php -dpcov.directory=. ./vendor/bin/phpunit --coverage-html ./coverage --configuration tests/php/integration/phpunit.xml.dist",
"php -dpcov.directory=. ./vendor/bin/phpunit --coverage-html ./coverage --configuration tests/php/unit/phpunit.xml.dist"
"php -dpcov.directory=. ./vendor/bin/phpunit-select-config tests/php/integration/phpunit.#.xml.dist --coverage-html ./coverage",
"php -dpcov.directory=. ./vendor/bin/phpunit-select-config tests/php/unit/phpunit.#.xml.dist --coverage-html ./coverage"
],
"test-php": [
"@composer phpunit"
@ -52,7 +52,7 @@
"link-template": "https://github.com/Automattic/jetpack-waf/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "0.18.x-dev"
"dev-trunk": "0.23.x-dev"
}
},
"config": {

View File

@ -6,6 +6,7 @@ use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Redirect;
use Jetpack_Options;
use WP_Error;
use WP_User;
/**
* Class Brute_Force_Protection_Blocked_Login_Page
@ -210,9 +211,13 @@ class Brute_Force_Protection_Blocked_Login_Page {
/**
* Check if user is blocked.
*
* @param string $user - the user.
* @param WP_User|WP_Error $user - The user or error object if prior callback failed auth.
*/
public function check_valid_blocked_user( $user ) {
if ( is_wp_error( $user ) ) {
return $user;
}
if ( $this->valid_blocked_user_id && $this->valid_blocked_user_id != $user->ID ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual
return new WP_Error( 'invalid_recovery_token', __( 'The recovery token is not valid for this user.', 'jetpack-waf' ) );
}
@ -769,6 +774,6 @@ class Brute_Force_Protection_Blocked_Login_Page {
</body>
</html>
<?php
die();
die( 0 );
}
}

View File

@ -139,20 +139,30 @@ class Brute_Force_Protection_Shared_Functions {
* @return object An IP Address object.
*/
private static function create_ip_object( $ip_address ) {
$range = false;
// Hyphenated range notation.
if ( strpos( $ip_address, '-' ) ) {
$ip_address = explode( '-', $ip_address );
$range = true;
$ip_range_parts = explode( '-', $ip_address );
return (object) array(
'range' => true,
'range_low' => trim( $ip_range_parts[0] ),
'range_high' => trim( $ip_range_parts[1] ),
);
}
$new_item = new \stdClass();
$new_item->range = $range;
if ( $range ) {
$new_item->range_low = trim( $ip_address[0] );
$new_item->range_high = trim( $ip_address[1] );
} else {
$new_item->ip_address = $ip_address;
// CIDR notation.
if ( strpos( $ip_address, '/' ) !== false ) {
return (object) array(
'range' => true,
'range_low' => $ip_address,
'range_high' => null,
);
}
return $new_item;
// Single IP Address.
return (object) array(
'range' => false,
'ip_address' => $ip_address,
);
}
/**

View File

@ -16,6 +16,46 @@ use Jetpack_Options;
*/
class Waf_Compatibility {
/**
* Returns the name for the IP allow list enabled/disabled option.
*
* @since 0.22.0
*
* @return string
*/
private static function get_ip_allow_list_enabled_option_name() {
/**
* Patch: bootstrap script generated prior to 0.17.0 may have autoloaded Waf_Rules_Manager class during standalone mode execution.
*
* @see peb6dq-2HL-p2
*/
if ( ! defined( 'Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME' ) ) {
return 'jetpack_waf_ip_allow_list_enabled';
}
return Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME;
}
/**
* Returns the name for the IP block list enabled/disabled option.
*
* @since 0.22.0
*
* @return string
*/
private static function get_ip_block_list_enabled_option_name() {
/**
* Patch: bootstrap script generated prior to 0.17.0 may have autoloaded Waf_Rules_Manager class during standalone mode execution.
*
* @see peb6dq-2HL-p2
*/
if ( ! defined( 'Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME' ) ) {
return 'jetpack_waf_ip_block_list_enabled';
}
return Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME;
}
/**
* Add compatibilty hooks
*
@ -28,8 +68,8 @@ class Waf_Compatibility {
add_filter( 'default_option_' . Waf_Initializer::NEEDS_UPDATE_OPTION_NAME, __CLASS__ . '::default_option_waf_needs_update', 10, 3 );
add_filter( 'default_option_' . Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, __CLASS__ . '::default_option_waf_ip_allow_list', 10, 3 );
add_filter( 'option_' . Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME, __CLASS__ . '::filter_option_waf_ip_allow_list', 10, 1 );
add_filter( 'default_option_' . Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME, __CLASS__ . '::default_option_waf_ip_allow_list_enabled', 10, 3 );
add_filter( 'default_option_' . Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME, __CLASS__ . '::default_option_waf_ip_block_list_enabled', 10, 3 );
add_filter( 'default_option_' . self::get_ip_allow_list_enabled_option_name(), __CLASS__ . '::default_option_waf_ip_allow_list_enabled', 10, 3 );
add_filter( 'default_option_' . self::get_ip_block_list_enabled_option_name(), __CLASS__ . '::default_option_waf_ip_block_list_enabled', 10, 3 );
}
/**

View File

@ -90,7 +90,15 @@ class REST_Controller {
* @return WP_REST_Response
*/
public static function waf() {
return rest_ensure_response( Waf_Runner::get_config() );
return rest_ensure_response(
array_merge(
Waf_Runner::get_config(),
array(
'waf_supported' => Waf_Runner::is_supported_environment(),
'automatic_rules_last_updated' => Waf_Stats::get_automatic_rules_last_updated(),
)
)
);
}
/**

View File

@ -0,0 +1,441 @@
<?php
/**
* Blocklog manager for the WAF
*
* @package automattic/jetpack-waf
*/
namespace Automattic\Jetpack\Waf;
/**
* Class used to manage blocklog operations
*/
class Waf_Blocklog_Manager {
const BLOCKLOG_OPTION_NAME_DAILY_SUMMARY = 'jetpack_waf_blocklog_daily_summary';
const BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT = 'jetpack_waf_all_time_block_count';
/**
* Database connection.
*
* @var \mysqli|null
*/
private static $db_connection = null;
/**
* Gets the path to the waf-blocklog file.
*
* @return string The waf-blocklog file path.
*/
public static function get_blocklog_file_path() {
return trailingslashit( JETPACK_WAF_DIR ) . 'waf-blocklog';
}
/**
* Connect to WordPress database.
*
* @return \mysqli|null
*/
private static function connect_to_wordpress_db() {
if ( self::$db_connection !== null ) {
return self::$db_connection;
}
if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
return null;
}
require_once JETPACK_WAF_WPCONFIG;
// @phan-suppress-next-line PhanUndeclaredConstant - These constants are defined in the wp-config file.
$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;
}
self::$db_connection = $conn;
return self::$db_connection;
}
/**
* Close the database connection.
*
* @return void
*/
private static function close_db_connection() {
if ( self::$db_connection ) {
self::$db_connection->close();
self::$db_connection = null;
}
}
/**
* Serialize a value for storage in a WordPress option.
*
* @param mixed $value The value to serialize.
* @return string The serialized value.
*/
private static function serialize_option_value( $value ) {
return serialize( $value );
}
/**
* Unserialize a value from a WordPress option.
*
* @param string $value The serialized value.
* @return mixed The unserialized value.
*/
private static function unserialize_option_value( string $value ) {
return unserialize( $value );
}
/**
* 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 );
}
/**
* Write block logs to database.
*
* @param array $log_data Log data.
*
* @return void
*/
private static function write_blocklog_row( $log_data ) {
$conn = self::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" );
}
}
}
/**
* Get the daily summary stats from the database.
*
* @return array The daily summary stats.
*/
private static function get_daily_summary() {
global $table_prefix;
$db_connection = self::connect_to_wordpress_db();
if ( ! $db_connection ) {
return array();
}
$result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY . "'" );
if ( ! $result ) {
return array();
}
$row = $result->fetch_assoc();
if ( ! $row ) {
return array();
}
$daily_summary = self::unserialize_option_value( $row['option_value'] );
$result->free();
return is_array( $daily_summary ) ? $daily_summary : array();
}
/**
* Increments the current date's daily summary stat.
*
* @param array $current_value The current value of the daily summary.
*
* @return array The updated daily summary.
*/
public static function increment_daily_summary( array $current_value ) {
$date = gmdate( 'Y-m-d' );
$value = intval( $current_value[ $date ] ?? 0 );
$current_value[ $date ] = $value + 1;
return $current_value;
}
/**
* Update the daily summary option in the database.
*
* @param array $value The value to update.
*
* @return void
*/
private static function write_daily_summary_row( array $value ) {
global $table_prefix;
$option_name = self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY;
$db_connection = self::connect_to_wordpress_db();
if ( ! $db_connection ) {
return;
}
$updated_value = self::serialize_option_value( $value );
$statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
if ( false !== $statement ) {
$statement->bind_param( 'sss', $option_name, $updated_value, $updated_value );
$statement->execute();
}
}
/**
* Update the daily summary stats for the current date.
*
* @return void
*/
private static function write_daily_summary() {
$stats = self::get_daily_summary();
$stats = self::increment_daily_summary( $stats );
$stats = self::filter_last_30_days( $stats );
self::write_daily_summary_row( $stats );
}
/**
* Get the all-time block count value from the database.
*
* @return int The all-time block count.
*/
private static function get_all_time_block_count_value() {
global $table_prefix;
$db_connection = self::connect_to_wordpress_db();
if ( ! $db_connection ) {
return 0;
}
$result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT . "'" );
if ( ! $result ) {
return 0;
}
$row = $result->fetch_assoc();
if ( ! $row ) {
return 0;
}
$all_time_block_count = intval( $row['option_value'] );
$result->free();
return $all_time_block_count;
}
/**
* Update the all-time block count value in the database.
*
* @param int $value The value to update.
* @return void
*/
private static function write_all_time_block_count_row( int $value ) {
global $table_prefix;
$option_name = self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT;
$db_connection = self::connect_to_wordpress_db();
if ( ! $db_connection ) {
return;
}
$statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
if ( false !== $statement ) {
$statement->bind_param( 'sii', $option_name, $value, $value );
$statement->execute();
}
}
/**
* Increment the all-time stats.
*
* @return void
*/
private static function write_all_time_block_count() {
$block_count = self::get_all_time_block_count_value();
if ( ! $block_count ) {
$block_count = self::get_default_all_time_stat_value();
}
self::write_all_time_block_count_row( $block_count + 1 );
}
/**
* Filters the stats to retain only data for the last 30 days.
*
* @param array $stats The array of stats to prune.
*
* @return array Pruned stats array.
*/
public static function filter_last_30_days( array $stats ) {
$today = gmdate( 'Y-m-d' );
$one_month_ago = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
return array_filter(
$stats,
function ( $date ) use ( $one_month_ago, $today ) {
return $date >= $one_month_ago && $date <= $today;
},
ARRAY_FILTER_USE_KEY
);
}
/**
* Get the total number of blocked requests for today.
*
* @return int
*/
public static function get_current_day_block_count() {
$stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
$today = gmdate( 'Y-m-d' );
return $stats[ $today ] ?? 0;
}
/**
* Get the total number of blocked requests for last thirty days.
*
* @return int
*/
public static function get_thirty_days_block_counts() {
$stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
$total_blocks = 0;
foreach ( $stats as $count ) {
$total_blocks += intval( $count );
}
return $total_blocks;
}
/**
* Get the total number of blocked requests for all time.
*
* @return int
*/
public static function get_all_time_block_count() {
$all_time_block_count = get_option( self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT, false );
if ( false !== $all_time_block_count ) {
return intval( $all_time_block_count );
}
return self::get_default_all_time_stat_value();
}
/**
* Compute the initial all-time stats value.
*
* @return int The initial all-time stats value.
*/
private static function get_default_all_time_stat_value() {
$conn = self::connect_to_wordpress_db();
if ( ! $conn ) {
return 0;
}
global $table_prefix;
$last_log_id_result = $conn->query( "SELECT log_id FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id DESC LIMIT 1" );
$all_time_block_count = 0;
if ( $last_log_id_result && $last_log_id_result->num_rows > 0 ) {
$row = $last_log_id_result->fetch_assoc();
if ( $row !== null && isset( $row['log_id'] ) ) {
$all_time_block_count = $row['log_id'];
}
}
return intval( $all_time_block_count );
}
/**
* Get the headers for logging purposes.
*
* @return array The headers.
*/
public static function get_request_headers() {
$all_headers = getallheaders();
$exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
foreach ( $exclude_headers as $header ) {
unset( $all_headers[ $header ] );
}
return $all_headers;
}
/**
* Write block logs. We won't write to the file if it exceeds 100 mb.
*
* @param string $rule_id The rule ID that triggered the block.
* @param string $reason The reason for the block.
*
* @return void
*/
public static 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' );
$log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['get_params'] = json_encode( $_GET );
if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
$log_data['post_params'] = json_encode( $_POST );
$log_data['headers'] = self::get_request_headers();
}
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 );
}
}
}
}
self::write_daily_summary();
self::write_all_time_block_count();
self::write_blocklog_row( $log_data );
self::close_db_connection();
}
}

View File

@ -142,6 +142,7 @@ class CLI extends WP_CLI_Command {
*/
public function generate_rules() {
try {
Waf_Constants::define_entrypoint();
Waf_Rules_Manager::generate_automatic_rules();
Waf_Rules_Manager::generate_rules();
} catch ( \Exception $e ) {
@ -159,7 +160,7 @@ class CLI extends WP_CLI_Command {
sprintf(
/* translators: %1$s is the name of the mode that was just switched to. */
__( 'Jetpack WAF rules successfully created to: "%1$s".', 'jetpack-waf' ),
Waf_Runner::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE )
Waf_Runner::get_waf_file_path( JETPACK_WAF_ENTRYPOINT )
)
);
}

View File

@ -22,6 +22,7 @@ class Waf_Constants {
self::define_waf_directory();
self::define_wpconfig_path();
self::define_killswitch();
self::define_entrypoint();
}
/**
@ -80,6 +81,15 @@ class Waf_Constants {
}
}
/**
* Set the entrypoint definition if it has not been set.
*/
public static function define_entrypoint() {
if ( ! defined( 'JETPACK_WAF_ENTRYPOINT' ) ) {
define( 'JETPACK_WAF_ENTRYPOINT', 'rules/rules.php' );
}
}
/**
* Set the share data definition if it has not been set.
*

View File

@ -329,27 +329,58 @@ class Waf_Request {
}
/**
* Returns the POST variables
* Returns the POST variables from a JSON body
*
* @return array{string, scalar}[]
*/
public function get_post_vars() {
private function get_json_post_vars() {
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
return flatten_array( $decoded_json, 'json', true );
}
/**
* Returns the POST variables from a urlencoded body
*
* @return array{string, scalar}[]
*/
private function get_urlencoded_post_vars() {
parse_str( $this->get_body(), $params );
return flatten_array( $params );
}
/**
* Returns the POST variables
*
* @param string $body_processor Manually specifiy the method to use to process the body. Options are 'URLENCODED' and 'JSON'.
*
* @return array{string, scalar}[]
*/
public function get_post_vars( string $body_processor = '' ) {
$content_type = $this->get_header( 'content-type' );
if ( ! empty( $_POST ) ) {
// If $_POST is populated, use it.
return flatten_array( $_POST );
} elseif ( strpos( $content_type, 'application/json' ) !== false ) {
// Attempt to decode JSON requests.
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
return flatten_array( $decoded_json, 'json', true );
} elseif ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
// Attempt to decode url-encoded data
parse_str( $this->get_body(), $params );
return flatten_array( $params );
} else {
// Don't try to parse any other content types
return array();
// If the body processor is specified by the rules file, trust it.
if ( 'URLENCODED' === $body_processor ) {
return $this->get_urlencoded_post_vars();
}
if ( 'JSON' === $body_processor ) {
return $this->get_json_post_vars();
}
// Otherwise, use $_POST if it's not empty.
if ( ! empty( $_POST ) ) {
return flatten_array( $_POST );
}
// Lastly, try to parse the body based on the content type.
if ( strpos( $content_type, 'application/json' ) !== false ) {
return $this->get_json_post_vars();
}
if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
return $this->get_urlencoded_post_vars();
}
// Don't try to parse any other content types.
return array();
}
/**

View File

@ -39,10 +39,16 @@ class Waf_Rules_Manager {
const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
// Rule Files
const AUTOMATIC_RULES_FILE = '/rules/automatic-rules.php';
const IP_ALLOW_RULES_FILE = '/rules/allow-ip.php';
const IP_BLOCK_RULES_FILE = '/rules/block-ip.php';
/**
* Rules Entrypoint File
*
* @deprecated 0.22.0 Use JETPACK_WAF_ENTRYPOINT instead.
*/
const RULES_ENTRYPOINT_FILE = '/rules/rules.php';
const AUTOMATIC_RULES_FILE = '/rules/automatic-rules.php';
const IP_ALLOW_RULES_FILE = '/rules/allow-ip.php';
const IP_BLOCK_RULES_FILE = '/rules/block-ip.php';
/**
* Whether automatic rules are enabled.
@ -221,9 +227,10 @@ class Waf_Rules_Manager {
public static function generate_rules() {
global $wp_filesystem;
Waf_Runner::initialize_filesystem();
Waf_Constants::define_entrypoint();
$rules = "<?php\n";
$entrypoint_file_path = Waf_Runner::get_waf_file_path( self::RULES_ENTRYPOINT_FILE );
$entrypoint_file_path = Waf_Runner::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
// Ensure that the folder exists
if ( ! $wp_filesystem->is_dir( dirname( $entrypoint_file_path ) ) ) {
@ -231,7 +238,7 @@ class Waf_Rules_Manager {
}
// Ensure all potentially required rule files exist
$rule_files = array( self::RULES_ENTRYPOINT_FILE, self::AUTOMATIC_RULES_FILE, self::IP_ALLOW_RULES_FILE, self::IP_BLOCK_RULES_FILE );
$rule_files = array( JETPACK_WAF_ENTRYPOINT, self::AUTOMATIC_RULES_FILE, self::IP_ALLOW_RULES_FILE, self::IP_BLOCK_RULES_FILE );
foreach ( $rule_files as $rule_file ) {
$rule_file = Waf_Runner::get_waf_file_path( $rule_file );
if ( ! $wp_filesystem->is_file( $rule_file ) ) {

View File

@ -31,6 +31,7 @@ class Waf_Runner {
return;
}
Waf_Constants::define_mode();
Waf_Constants::define_entrypoint();
Waf_Constants::define_share_data();
if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
@ -256,7 +257,7 @@ class Waf_Runner {
$waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() );
// execute waf rules.
$rules_file_path = self::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE );
$rules_file_path = self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
if ( file_exists( $rules_file_path ) ) {
// phpcs:ignore
include $rules_file_path;
@ -326,7 +327,7 @@ class Waf_Runner {
Waf_Rules_Manager::generate_ip_rules();
Waf_Rules_Manager::generate_rules();
self::create_blocklog_table();
Waf_Blocklog_Manager::create_blocklog_table();
}
/**
@ -353,30 +354,6 @@ class Waf_Runner {
}
}
/**
* 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.
*
@ -390,14 +367,15 @@ class Waf_Runner {
global $wp_filesystem;
self::initialize_filesystem();
Waf_Constants::define_entrypoint();
// If the rules file doesn't exist, there's nothing else to do.
if ( ! $wp_filesystem->exists( self::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE ) ) ) {
if ( ! $wp_filesystem->exists( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ) ) ) {
return;
}
// Empty the rules entrypoint file.
if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( Waf_Rules_Manager::RULES_ENTRYPOINT_FILE ), "<?php\n" ) ) {
if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ), "<?php\n" ) ) {
throw new File_System_Exception( 'Failed to empty rules.php file.' );
}
}

View File

@ -38,6 +38,14 @@ class Waf_Runtime {
*/
const NORMALIZE_ARRAY_MATCH_VALUES = 2;
/**
* The version of this runtime class. Used by rule files to ensure compatibility.
*
* @since 0.21.0
*
* @var int
*/
public $version = 1;
/**
* Last rule.
*
@ -68,6 +76,12 @@ class Waf_Runtime {
* @var string
*/
public $matched_var_name = '';
/**
* Body Processor.
*
* @var string 'URLENCODED' | 'JSON' | ''
*/
private $body_processor = '';
/**
* State.
@ -271,7 +285,7 @@ class Waf_Runtime {
$reason = $this->sanitize_output( $reason );
}
$this->write_blocklog( $rule_id, $reason );
Waf_Blocklog_Manager::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 ) {
@ -281,106 +295,6 @@ class Waf_Runtime {
}
}
/**
* Get the headers for logging purposes.
*/
public function get_request_headers() {
$all_headers = getallheaders();
$exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
foreach ( $exclude_headers as $header ) {
unset( $all_headers[ $header ] );
}
return $all_headers;
}
/**
* 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' );
$log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['get_params'] = json_encode( $_GET );
if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
$log_data['post_params'] = json_encode( $_POST );
$log_data['headers'] = $this->get_request_headers();
}
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.
*
@ -391,7 +305,7 @@ class Waf_Runtime {
public function redirect( $rule_id, $url ) {
error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" );
header( "Location: $url" );
exit;
exit( 0 );
}
/**
@ -538,7 +452,7 @@ class Waf_Runtime {
$value = $this->args_names( $this->meta( 'args_get' ) );
break;
case 'args_post':
$value = $this->request->get_post_vars();
$value = $this->request->get_post_vars( $this->get_body_processor() );
break;
case 'args_post_names':
$value = $this->args_names( $this->meta( 'args_post' ) );
@ -588,6 +502,28 @@ class Waf_Runtime {
return $output;
}
/**
* Get the body processor.
*
* @return string
*/
private function get_body_processor() {
return $this->body_processor;
}
/**
* Set the body processor.
*
* @param string $processor Processor to set. Either 'URLENCODED' or 'JSON'.
*
* @return void
*/
public function set_body_processor( $processor ) {
if ( $processor === 'URLENCODED' || $processor === 'JSON' ) {
$this->body_processor = $processor;
}
}
/**
* Change a string to all lowercase and replace spaces and underscores with dashes.
*
@ -680,7 +616,7 @@ class Waf_Runtime {
continue 2;
default:
var_dump( 'Unknown target', $k, $v );
exit;
exit( 0 );
}
$return[] = array(
'name' => $k,
@ -703,8 +639,8 @@ class Waf_Runtime {
$array_length = count( $array );
for ( $i = 0; $i < $array_length; $i++ ) {
// Check if the IP matches a provided range.
$range = explode( '-', $array[ $i ] );
// Check if the IP matches a provided range or CIDR notation.
$range = strpos( $array[ $i ], '/' ) !== false ? array( $array[ $i ], null ) : explode( '-', $array[ $i ] );
if ( count( $range ) === 2 ) {
if ( IP_Utils::ip_address_is_in_range( $real_ip, $range[0], $range[1] ) ) {
return true;

View File

@ -120,6 +120,15 @@ class Waf_Standalone_Bootstrap {
return trailingslashit( JETPACK_WAF_DIR ) . 'bootstrap.php';
}
/**
* Gets the entrypoint file.
*
* @return string The entrypoint file.
*/
private function get_entrypoint() {
return defined( 'JETPACK_WAF_ENTRYPOINT' ) ? JETPACK_WAF_ENTRYPOINT : 'rules/rules.php';
}
/**
* Generates the bootstrap file.
*
@ -141,6 +150,7 @@ class Waf_Standalone_Bootstrap {
$autoloader_file = $this->locate_autoloader_file();
$bootstrap_file = $this->get_bootstrap_file_path();
$entrypoint = $this->get_entrypoint();
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
@ -154,6 +164,7 @@ class Waf_Standalone_Bootstrap {
. sprintf( "define( 'JETPACK_WAF_SHARE_DEBUG_DATA', %s );\n", var_export( $share_debug_data_option, true ) )
. sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) )
. sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) )
. sprintf( "define( 'JETPACK_WAF_ENTRYPOINT', %s );\n", var_export( $entrypoint, true ) )
. 'require_once ' . var_export( $autoloader_file, true ) . ";\n"
. "Automattic\Jetpack\Waf\Waf_Runner::initialize();\n";
// phpcs:enable

View File

@ -15,18 +15,28 @@ use Automattic\Jetpack\IP\Utils as IP_Utils;
class Waf_Stats {
/**
* The global stats cache
* Retrieve blocked requests from database
*
* @var array|null
* @return array
*/
public static $global_stats = null;
public static function get_blocked_requests() {
return array(
'current_day' => Waf_Blocklog_Manager::get_current_day_block_count(),
'thirty_days' => Waf_Blocklog_Manager::get_thirty_days_block_counts(),
'all_time' => Waf_Blocklog_Manager::get_all_time_block_count(),
);
}
/**
* Get IP allow list count
*
* @return int The number of valid IP addresses in the allow list
*
* @deprecated 0.20.1 Use Automattic\Jetpack\Waf\Waf_Blocklog_Manager API instead.
*/
public static function get_ip_allow_list_count() {
_deprecated_function( __METHOD__, 'waf-0.20.1', 'Automattic\Jetpack\Waf\Waf_Blocklog_Manager' );
$ip_allow_list = get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME );
if ( ! $ip_allow_list ) {
@ -42,8 +52,12 @@ class Waf_Stats {
* Get IP block list count
*
* @return int The number of valid IP addresses in the block list
*
* @deprecated 0.20.1 Use Automattic\Jetpack\Waf\Waf_Blocklog_Manager API instead.
*/
public static function get_ip_block_list_count() {
_deprecated_function( __METHOD__, 'waf-0.20.1', 'Automattic\Jetpack\Waf\Waf_Blocklog_Manager' );
$ip_block_list = get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME );
if ( ! $ip_block_list ) {
@ -55,6 +69,12 @@ class Waf_Stats {
return count( $results );
}
/** The global stats cache
*
* @var array|null
*/
public static $global_stats = null;
/**
* Get Rules version
*

View File

@ -70,3 +70,51 @@ function flatten_array( $array, $key_prefix = '', $dot_notation = null ) {
}
return $return;
}
/**
* Polyfill for getallheaders, which is not available in all PHP environments.
*
* @link https://github.com/ralouphie/getallheaders
*/
if ( ! function_exists( 'getallheaders' ) ) {
/**
* Get all HTTP header key/values as an associative array for the current request.
*
* @return array The HTTP header key/value pairs.
*/
function getallheaders() {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput
$headers = array();
$copy_server = array(
'CONTENT_TYPE' => 'Content-Type',
'CONTENT_LENGTH' => 'Content-Length',
'CONTENT_MD5' => 'Content-Md5',
);
foreach ( $_SERVER as $key => $value ) {
if ( substr( $key, 0, 5 ) === 'HTTP_' ) {
$key = substr( $key, 5 );
if ( ! isset( $copy_server[ $key ] ) || ! isset( $_SERVER[ $key ] ) ) {
$key = str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', $key ) ) ) );
$headers[ $key ] = $value;
}
} elseif ( isset( $copy_server[ $key ] ) ) {
$headers[ $copy_server[ $key ] ] = $value;
}
}
if ( ! isset( $headers['Authorization'] ) ) {
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif ( isset( $_SERVER['PHP_AUTH_USER'] ) ) {
$basic_pass = $_SERVER['PHP_AUTH_PW'] ?? '';
$headers['Authorization'] = 'Basic ' . base64_encode( $_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass );
} elseif ( isset( $_SERVER['PHP_AUTH_DIGEST'] ) ) {
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
}