updated plugin Jetpack Protect
version 4.0.0
This commit is contained in:
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 ) ) {
|
||||
|
@ -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.' );
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user