From 4d086cc4302bfdfc63f0ff4e28aff66d9d8bc3c5 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 14:32:37 +0300 Subject: [PATCH 01/23] Make the plugin responsible for resolving request IPs --- classes/class-plugin.php | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 376e0a170..c74f0a221 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -90,6 +90,13 @@ class Plugin { */ public $locations = array(); + /** + * IP address for the current request to be associated with the log entry. + * + * @var null|false|string + */ + protected $client_ip_address; + /** * Class constructor */ @@ -138,6 +145,9 @@ public function __construct() { // Load logger class. $this->log = apply_filters( 'wp_stream_log_handler', new Log( $this ) ); + // Set the IP address for the current request. + $this->client_ip_address = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ); + // Load settings and connectors after widgets_init and before the default init priority. add_action( 'init', array( $this, 'init' ), 9 ); @@ -315,4 +325,48 @@ public function is_mustuse() { return false; } + + /** + * Get the IP address for the current request. + * + * @return false|null|string + */ + public function get_client_ip_address() { + return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address ); + } + + /** + * Get the client IP address from the HTTP request headers. + * + * There is no guarantee that this is the real IP address of the client. + * + * @return string|null + */ + protected function get_unsafe_client_ip_address() { + // List of $_SERVER keys that could contain the client IP address. + $address_headers = array( + 'HTTP_X_FORWARDED_FOR', + 'HTTP_FORWARDED_FOR', + ); + + foreach ( $address_headers as $header ) { + if ( ! empty( $_SERVER[ $header ] ) ) { + $header_client_ip = $_SERVER[ $header ]; + + // Account for multiple IPs in case of multiple proxies. + if ( false !== strpos( $header_client_ip, ',' ) ) { + $header_client_ips = explode( ',', $header_client_ip ); + $header_client_ip = $header_client_ips[0]; + } + + $client_ip = filter_var( trim( $header_client_ip ), FILTER_VALIDATE_IP ); + + if ( ! empty( $client_ip ) ) { + return $client_ip; + } + } + } + + return null; + } } From 868957bd8e21cc28acffc6636bcc507f5513edf1 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 14:33:28 +0300 Subject: [PATCH 02/23] Make the logger rely on the IP resolved by the plugin --- classes/class-log.php | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/classes/class-log.php b/classes/class-log.php index b13b2beb8..2e26efd2e 100644 --- a/classes/class-log.php +++ b/classes/class-log.php @@ -19,14 +19,6 @@ class Log { */ public $plugin; - /** - * Hold Current visitors IP Address. - * - * @var string - */ - private $ip_address; - - /** * Previous Stream record ID, used for chaining same-session records * @@ -42,12 +34,6 @@ class Log { public function __construct( $plugin ) { $this->plugin = $plugin; - // Support proxy mode by checking the `X-Forwarded-For` header first. - $ip_address = wp_stream_filter_input( INPUT_SERVER, 'HTTP_X_FORWARDED_FOR', FILTER_VALIDATE_IP ); - $ip_address = $ip_address ? $ip_address : wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ); - - $this->ip_address = $ip_address; - // Ensure function used in various methods is pre-loaded. if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; @@ -87,9 +73,16 @@ public function log( $connector, $message, $args, $object_id, $context, $action, return false; } + $ip_address = $this->plugin->get_client_ip_address(); + + // Fallback to unsafe IP extracted from the request HTTP headers. + if ( empty( $ip_address ) ) { + $ip_address = $this->plugin->get_unsafe_client_ip_address(); + } + $user = new \WP_User( $user_id ); - if ( $this->is_record_excluded( $connector, $context, $action, $user ) ) { + if ( $this->is_record_excluded( $connector, $context, $action, $user, $ip_address ) ) { return false; } @@ -140,7 +133,7 @@ function ( $var ) { 'connector' => (string) $connector, 'context' => (string) $context, 'action' => (string) $action, - 'ip' => (string) $this->ip_address, + 'ip' => (string) $ip_address, 'meta' => (array) $stream_meta, ); @@ -174,12 +167,6 @@ public function is_record_excluded( $connector, $context, $action, $user = null, $user = wp_get_current_user(); } - if ( is_null( $ip ) ) { - $ip = $this->ip_address; - } else { - $ip = wp_stream_filter_var( $ip, FILTER_VALIDATE_IP ); - } - if ( ! empty( $user->roles ) ) { $roles = array_values( $user->roles ); $role = $roles[0]; From 970121b9b6a7762baeb9917fc9d1885a5cc531c7 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 14:34:10 +0300 Subject: [PATCH 03/23] Add a notice to plugin settings in case request IP can't be determined --- classes/class-admin.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/classes/class-admin.php b/classes/class-admin.php index 4a29b2ed0..6e0c67e6d 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -220,6 +220,12 @@ public function init() { $this->network = new Network( $this->plugin ); $this->live_update = new Live_Update( $this->plugin ); $this->export = new Export( $this->plugin ); + + // Check if the host has configured the `REMOTE_ADDR` correctly. + $client_ip = $this->plugin->get_client_ip_address(); + if ( empty( $client_ip ) ) { + $this->notice( esc_html__( 'Stream can\'t determine a reliable client IP address! Please update the hosting environment to set the REMOTE_ADDR in $_SERVER variable or use the `wp_stream_client_ip_address` filter to specify the verified client IP address!', 'stream' ) ); + } } /** From 55780662f7ef74f83b692d218325e9f672e319e3 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 14:34:24 +0300 Subject: [PATCH 04/23] Document the IP resolver logic --- readme.txt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/readme.txt b/readme.txt index 41f4d145b..37e2855e7 100644 --- a/readme.txt +++ b/readme.txt @@ -65,12 +65,25 @@ With Stream’s powerful logging, you’ll have the valuable information you nee * WP-CLI command for querying records -= Known Issues +## Configuration + +Most of the plugin configuration is available under the "Stream" → "Settings" in the WordPress dashboard. + +### Request IP Address + +The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. + +If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extract the client IP from `$_SERVER['HTTP_X_FORWARDED_FOR']` and `$_SERVER['HTTP_FORWARDED_FOR']` *which are considered unsafe as they can contain arbitraty user input passed with the HTTP request*. This fallback behaviour will be disabled by default in the future versions of this plugin! + +Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that. + + +## Known Issues * We have temporarily disabled the data removal feature through plugin uninstallation, starting with version 3.9.3. We identified a few edge cases that did not behave as expected and we decided that a temporary removal is preferable at this time for such an impactful and irreversible operation. Our team is actively working on refining this feature to ensure it performs optimally and securely. We plan to reintroduce it in a future update with enhanced safeguards. -= Contribute = +## Contribute There are several ways you can get involved to help make Stream better: From 54150d64be0ac39ccbb079b49b178580146bbab1 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 14:43:30 +0300 Subject: [PATCH 05/23] Add an example --- readme.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 37e2855e7..4250d7dd9 100644 --- a/readme.txt +++ b/readme.txt @@ -75,7 +75,19 @@ The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verifie If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extract the client IP from `$_SERVER['HTTP_X_FORWARDED_FOR']` and `$_SERVER['HTTP_FORWARDED_FOR']` *which are considered unsafe as they can contain arbitraty user input passed with the HTTP request*. This fallback behaviour will be disabled by default in the future versions of this plugin! -Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that. +Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: + +`add_filter( + 'wp_stream_client_ip_address', + function( $client_ip ) { + // Trust the X-Forwarded-For header. + if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + return $_SERVER['HTTP_X_FORWARDED_FOR']; + } + + return $client_ip; + } +);` ## Known Issues From 2a00ddf04a59c33f8cb23f596fb3a9a4b367ded8 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:11:19 +0300 Subject: [PATCH 06/23] Limit the notice to the Stream admin pages --- classes/class-admin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/class-admin.php b/classes/class-admin.php index 6e0c67e6d..259165f56 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -223,8 +223,8 @@ public function init() { // Check if the host has configured the `REMOTE_ADDR` correctly. $client_ip = $this->plugin->get_client_ip_address(); - if ( empty( $client_ip ) ) { - $this->notice( esc_html__( 'Stream can\'t determine a reliable client IP address! Please update the hosting environment to set the REMOTE_ADDR in $_SERVER variable or use the `wp_stream_client_ip_address` filter to specify the verified client IP address!', 'stream' ) ); + if ( ! empty( $client_ip ) && $this->is_stream_screen() ) { + $this->notice( __( 'Stream plugin can\'t determine a reliable client IP address! Please update the hosting environment to set the $_SERVER[\'REMOTE_ADDR\'] variable or use the wp_stream_client_ip_address filter to specify the verified client IP address!', 'stream' ) ); } } From c637256a99c0be79a982158b99bf340281bfbc11 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:11:38 +0300 Subject: [PATCH 07/23] Ensure we fail gracefully when the WP core helper is not available --- classes/class-admin.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/classes/class-admin.php b/classes/class-admin.php index 259165f56..cb68b6ac3 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -547,9 +547,10 @@ public function is_stream_screen() { return true; } - $screen = get_current_screen(); - if ( Alerts::POST_TYPE === $screen->post_type ) { - return true; + if ( is_admin() && function_exists( 'get_current_screen' ) ) { + $screen = get_current_screen(); + + return ( Alerts::POST_TYPE === $screen->post_type ); } return false; From fb0f09301afb8909c81db38cd939d11c0966a0db Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:24:49 +0300 Subject: [PATCH 08/23] Fix docblock syntax --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 186fafe81..7bd438413 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -16,7 +16,7 @@ * @param int $filter The ID of the filter to apply. * @param mixed $options Associative array of options or bitwise disjunction of flags. If filter accepts options, flags can be provided in "flags" field of array. * - * @return Value of the requested variable on success, FALSE if the filter fails, or NULL if the $variable_name is not set. + * @return mixed|false|null Value of the requested variable on success, FALSE if the filter fails, or NULL if the $variable_name is not set. */ function wp_stream_filter_input( $type, $variable_name, $filter = null, $options = array() ) { return call_user_func_array( array( '\WP_Stream\Filter_Input', 'super' ), func_get_args() ); From c10f996547915dd1e525482dfdae61b783312a52 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:26:54 +0300 Subject: [PATCH 09/23] Make it accessible --- classes/class-plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-plugin.php b/classes/class-plugin.php index c74f0a221..31f4f94d3 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -342,7 +342,7 @@ public function get_client_ip_address() { * * @return string|null */ - protected function get_unsafe_client_ip_address() { + public function get_unsafe_client_ip_address() { // List of $_SERVER keys that could contain the client IP address. $address_headers = array( 'HTTP_X_FORWARDED_FOR', From d18e872b41678cbef1a55112c4699a411f62cb42 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 12:27:57 +0000 Subject: [PATCH 10/23] Add basic tests --- tests/tests/test-class-plugin.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/tests/test-class-plugin.php b/tests/tests/test-class-plugin.php index 6d6392372..f006fb13f 100644 --- a/tests/tests/test-class-plugin.php +++ b/tests/tests/test-class-plugin.php @@ -84,4 +84,18 @@ public function test_get_version() { $version = $this->plugin->get_version(); $this->assertNotEmpty( $version ); } + + public function test_get_client_ip_address() { + $this->assertEquals( $_SERVER['REMOTE_ADDR'], $this->plugin->get_client_ip_address() ); + } + + public function test_get_unsafe_client_ip_address() { + $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123,321.123.123.123'; + + $this->assertEquals( + '123.123.123.123', + $this->plugin->get_unsafe_client_ip_address(), + 'Use the first IP from the list' + ); + } } From 3d11efd22a6b762ed9ae4ca6e94e3d03c15863a2 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:30:53 +0300 Subject: [PATCH 11/23] Test for invalid IP --- tests/tests/test-class-plugin.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/tests/test-class-plugin.php b/tests/tests/test-class-plugin.php index f006fb13f..642177936 100644 --- a/tests/tests/test-class-plugin.php +++ b/tests/tests/test-class-plugin.php @@ -97,5 +97,13 @@ public function test_get_unsafe_client_ip_address() { $this->plugin->get_unsafe_client_ip_address(), 'Use the first IP from the list' ); + + $_SERVER['HTTP_X_FORWARDED_FOR'] = '827.invalid-ip'; + + $this->assertEquals( + false, + $this->plugin->get_unsafe_client_ip_address(), + 'Invalid IP format should fail the validation' + ); } } From d9fa9856d03d0362a7ba00e38c5850782e93823b Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 15:51:24 +0300 Subject: [PATCH 12/23] Use the local helper for consistency --- classes/class-plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 31f4f94d3..0eb31e583 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -359,7 +359,7 @@ public function get_unsafe_client_ip_address() { $header_client_ip = $header_client_ips[0]; } - $client_ip = filter_var( trim( $header_client_ip ), FILTER_VALIDATE_IP ); + $client_ip = wp_stream_filter_var( trim( $header_client_ip ), FILTER_VALIDATE_IP ); if ( ! empty( $client_ip ) ) { return $client_ip; From 4afe6ec457f8f270612fc1229bbb0152fcbe4051 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 9 Oct 2023 20:38:35 +0300 Subject: [PATCH 13/23] Update readme.txt Co-authored-by: Alain Schlesser --- readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 4250d7dd9..064283465 100644 --- a/readme.txt +++ b/readme.txt @@ -67,7 +67,8 @@ With Stream’s powerful logging, you’ll have the valuable information you nee ## Configuration -Most of the plugin configuration is available under the "Stream" → "Settings" in the WordPress dashboard. +Most of the plugin configuration is available under the "Stream" → "Settings" page in the WordPress dashboard. + ### Request IP Address From f0a5e20e0153b5ff410914b611c0b610a855e9f5 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 09:54:38 +0300 Subject: [PATCH 14/23] Apply suggestions from code review Co-authored-by: Alain Schlesser --- readme.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 064283465..47bf61ce4 100644 --- a/readme.txt +++ b/readme.txt @@ -74,7 +74,8 @@ Most of the plugin configuration is available under the "Stream" → "Settings" The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. -If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extract the client IP from `$_SERVER['HTTP_X_FORWARDED_FOR']` and `$_SERVER['HTTP_FORWARDED_FOR']` *which are considered unsafe as they can contain arbitraty user input passed with the HTTP request*. This fallback behaviour will be disabled by default in the future versions of this plugin! +If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extract the client IP from `$_SERVER['HTTP_X_FORWARDED_FOR']` or `$_SERVER['HTTP_FORWARDED_FOR']` *which are considered unsafe as they can contain arbitrary user input passed with the HTTP request*. This fallback behaviour will be disabled by default in future versions of this plugin! + Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: From a136099f34df1fbc4843f654b9fb2e8dc271e91b Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 09:55:15 +0300 Subject: [PATCH 15/23] Apply suggestions from code review Co-authored-by: Alain Schlesser --- tests/tests/test-class-plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/test-class-plugin.php b/tests/tests/test-class-plugin.php index 642177936..010ebf62b 100644 --- a/tests/tests/test-class-plugin.php +++ b/tests/tests/test-class-plugin.php @@ -90,7 +90,7 @@ public function test_get_client_ip_address() { } public function test_get_unsafe_client_ip_address() { - $_SERVER['HTTP_X_FORWARDED_FOR'] = '123.123.123.123,321.123.123.123'; + $_SERVER['HTTP_X_FORWARDED_FOR'] = ' 123.123.123.123 , 321.123.123.123, 456.123.123.123 '; $this->assertEquals( '123.123.123.123', From 560baf1f86dc15d95041a68b087d07a817626fc6 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 09:59:33 +0300 Subject: [PATCH 16/23] Skip any formatting for simplicity --- classes/class-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-admin.php b/classes/class-admin.php index cb68b6ac3..fc23a1c08 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -224,7 +224,7 @@ public function init() { // Check if the host has configured the `REMOTE_ADDR` correctly. $client_ip = $this->plugin->get_client_ip_address(); if ( ! empty( $client_ip ) && $this->is_stream_screen() ) { - $this->notice( __( 'Stream plugin can\'t determine a reliable client IP address! Please update the hosting environment to set the $_SERVER[\'REMOTE_ADDR\'] variable or use the wp_stream_client_ip_address filter to specify the verified client IP address!', 'stream' ) ); + $this->notice( __( 'Stream plugin can\'t determine a reliable client IP address! Please update the hosting environment to set the $_SERVER[\'REMOTE_ADDR\'] variable or use the wp_stream_client_ip_address filter to specify the verified client IP address!', 'stream' ) ); } } From 94e17bdf2e8a8eb2bb9fb84666be9dc6e2f40647 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:07:34 +0300 Subject: [PATCH 17/23] Describe the output types --- classes/class-plugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 0eb31e583..6e70b83af 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -93,7 +93,7 @@ class Plugin { /** * IP address for the current request to be associated with the log entry. * - * @var null|false|string + * @var null|false|string Valid IP address, null if not set, false if invalid. */ protected $client_ip_address; @@ -329,7 +329,7 @@ public function is_mustuse() { /** * Get the IP address for the current request. * - * @return false|null|string + * @return false|null|string Valid IP address, null if not set, false if invalid. */ public function get_client_ip_address() { return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address ); From 9daaaa8f735b91efa087485c3159826f63b429cc Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:13:59 +0300 Subject: [PATCH 18/23] Account for multiple IPs in the forwarded header --- readme.txt | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/readme.txt b/readme.txt index 47bf61ce4..8503f6b08 100644 --- a/readme.txt +++ b/readme.txt @@ -80,15 +80,19 @@ If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extra Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: `add_filter( - 'wp_stream_client_ip_address', - function( $client_ip ) { - // Trust the X-Forwarded-For header. - if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { - return $_SERVER['HTTP_X_FORWARDED_FOR']; - } - - return $client_ip; - } + 'wp_stream_client_ip_address', + function( $client_ip ) { + // Trust the first IP in the X-Forwarded-For header. + if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + $forwarded_ips = explode( ',' $_SERVER['HTTP_X_FORWARDED_FOR'] ); + + if ( filter_var( $forwarded_ips[0], FILTER_VALIDATE_IP ) ) { + return $forwarded_ips[0]; + } + } + + return $client_ip; + } );` From 1b9068da98e96b64e21f4e4c06ad8fd40b9a9200 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:45:44 +0300 Subject: [PATCH 19/23] Show the notice if client IP missing --- classes/class-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/class-admin.php b/classes/class-admin.php index fc23a1c08..6b0dd3eb7 100644 --- a/classes/class-admin.php +++ b/classes/class-admin.php @@ -223,7 +223,7 @@ public function init() { // Check if the host has configured the `REMOTE_ADDR` correctly. $client_ip = $this->plugin->get_client_ip_address(); - if ( ! empty( $client_ip ) && $this->is_stream_screen() ) { + if ( empty( $client_ip ) && $this->is_stream_screen() ) { $this->notice( __( 'Stream plugin can\'t determine a reliable client IP address! Please update the hosting environment to set the $_SERVER[\'REMOTE_ADDR\'] variable or use the wp_stream_client_ip_address filter to specify the verified client IP address!', 'stream' ) ); } } From 38683c13a5d0ca7031a24b69b8bd492b8fae6214 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:45:57 +0300 Subject: [PATCH 20/23] =?UTF-8?q?Don=E2=80=99t=20even=20attempt=20to=20use?= =?UTF-8?q?=20the=20unsafe=20option?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- classes/class-log.php | 5 ----- classes/class-plugin.php | 35 ------------------------------- tests/tests/test-class-plugin.php | 18 ---------------- 3 files changed, 58 deletions(-) diff --git a/classes/class-log.php b/classes/class-log.php index 2e26efd2e..2a9f6c681 100644 --- a/classes/class-log.php +++ b/classes/class-log.php @@ -75,11 +75,6 @@ public function log( $connector, $message, $args, $object_id, $context, $action, $ip_address = $this->plugin->get_client_ip_address(); - // Fallback to unsafe IP extracted from the request HTTP headers. - if ( empty( $ip_address ) ) { - $ip_address = $this->plugin->get_unsafe_client_ip_address(); - } - $user = new \WP_User( $user_id ); if ( $this->is_record_excluded( $connector, $context, $action, $user, $ip_address ) ) { diff --git a/classes/class-plugin.php b/classes/class-plugin.php index 6e70b83af..170c472bc 100755 --- a/classes/class-plugin.php +++ b/classes/class-plugin.php @@ -334,39 +334,4 @@ public function is_mustuse() { public function get_client_ip_address() { return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address ); } - - /** - * Get the client IP address from the HTTP request headers. - * - * There is no guarantee that this is the real IP address of the client. - * - * @return string|null - */ - public function get_unsafe_client_ip_address() { - // List of $_SERVER keys that could contain the client IP address. - $address_headers = array( - 'HTTP_X_FORWARDED_FOR', - 'HTTP_FORWARDED_FOR', - ); - - foreach ( $address_headers as $header ) { - if ( ! empty( $_SERVER[ $header ] ) ) { - $header_client_ip = $_SERVER[ $header ]; - - // Account for multiple IPs in case of multiple proxies. - if ( false !== strpos( $header_client_ip, ',' ) ) { - $header_client_ips = explode( ',', $header_client_ip ); - $header_client_ip = $header_client_ips[0]; - } - - $client_ip = wp_stream_filter_var( trim( $header_client_ip ), FILTER_VALIDATE_IP ); - - if ( ! empty( $client_ip ) ) { - return $client_ip; - } - } - } - - return null; - } } diff --git a/tests/tests/test-class-plugin.php b/tests/tests/test-class-plugin.php index 010ebf62b..3bbc50f18 100644 --- a/tests/tests/test-class-plugin.php +++ b/tests/tests/test-class-plugin.php @@ -88,22 +88,4 @@ public function test_get_version() { public function test_get_client_ip_address() { $this->assertEquals( $_SERVER['REMOTE_ADDR'], $this->plugin->get_client_ip_address() ); } - - public function test_get_unsafe_client_ip_address() { - $_SERVER['HTTP_X_FORWARDED_FOR'] = ' 123.123.123.123 , 321.123.123.123, 456.123.123.123 '; - - $this->assertEquals( - '123.123.123.123', - $this->plugin->get_unsafe_client_ip_address(), - 'Use the first IP from the list' - ); - - $_SERVER['HTTP_X_FORWARDED_FOR'] = '827.invalid-ip'; - - $this->assertEquals( - false, - $this->plugin->get_unsafe_client_ip_address(), - 'Invalid IP format should fail the validation' - ); - } } From f6c799b3793aaeb00dc13ebc7f1687b09e286d03 Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:50:06 +0300 Subject: [PATCH 21/23] We no longer default to a fallback --- readme.txt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/readme.txt b/readme.txt index 8503f6b08..0574a8030 100644 --- a/readme.txt +++ b/readme.txt @@ -72,12 +72,7 @@ Most of the plugin configuration is available under the "Stream" → "Settings" ### Request IP Address -The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. - -If `$_SERVER['REMOTE_ADDR']` is not configured, the plugin will attempt to extract the client IP from `$_SERVER['HTTP_X_FORWARDED_FOR']` or `$_SERVER['HTTP_FORWARDED_FOR']` *which are considered unsafe as they can contain arbitrary user input passed with the HTTP request*. This fallback behaviour will be disabled by default in future versions of this plugin! - - -Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: +The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: `add_filter( 'wp_stream_client_ip_address', From 85bdc4db446ef07d120185cbff01493c3445935f Mon Sep 17 00:00:00 2001 From: Kaspars Dambis Date: Mon, 16 Oct 2023 10:54:01 +0300 Subject: [PATCH 22/23] Add the changelog --- readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.txt b/readme.txt index 0574a8030..363447b1a 100644 --- a/readme.txt +++ b/readme.txt @@ -129,6 +129,10 @@ Track changes to posts when using the block editor. == Changelog == += NEXT = + +- Breaking: Use only `$_SERVER['REMOTE_ADDR']` as the reliable client IP address for event logs. This might cause incorrectly reported event log IP addresses on environments where PHP is behind a proxy server or CDN. Use the `wp_stream_client_ip_address` filter to set the correct client IP address (see `readme.txt` for instructions) or configure the hosting environment to report the correct IP address in `$_SERVER['REMOTE_ADDR']`. + = 3.10.0 - October 9, 2023 = - Fix: Improve PHP 8.1 compatibility by updating `filter_*()` calls referencing `FILTER_SANITIZE_STRING` (issue [#1422](https://github.com/xwp/stream/pull/1422)). From 9bcc4906d6b3bfac6bda0e665d1d650f3ffcc697 Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Mon, 16 Oct 2023 17:44:01 +0200 Subject: [PATCH 23/23] Add noticeable warning regarding HTTP_* spoofing --- readme.txt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/readme.txt b/readme.txt index 363447b1a..8315816d9 100644 --- a/readme.txt +++ b/readme.txt @@ -65,19 +65,22 @@ With Stream’s powerful logging, you’ll have the valuable information you nee * WP-CLI command for querying records -## Configuration +== Configuration == Most of the plugin configuration is available under the "Stream" → "Settings" page in the WordPress dashboard. -### Request IP Address += Request IP Address = -The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address or use the `wp_stream_client_ip_address` filter to do that: +The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verified IP address of the current request. On hosting environments with PHP processing behind reverse proxies or CDNs the actual client IP is passed to PHP through request HTTP headers such as `X-Forwarded-For` and `True-Client-IP` which can't be trusted without an additional layer of validation. Update your server configuration to set the `$_SERVER['REMOTE_ADDR']` variable to the verified client IP address. + +As a workaround, you can use the `wp_stream_client_ip_address` filter to adapt the IP address: `add_filter( 'wp_stream_client_ip_address', function( $client_ip ) { // Trust the first IP in the X-Forwarded-For header. + // ⚠️ Note: This is inherently insecure and can easily be spoofed! if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $forwarded_ips = explode( ',' $_SERVER['HTTP_X_FORWARDED_FOR'] ); @@ -90,13 +93,15 @@ The plugin expects the `$_SERVER['REMOTE_ADDR']` variable to contain the verifie } );` +⚠️ **WARNING:** The above is an insecure workaround that you should only use when you fully understand what this implies. Relying on any variable with the `HTTP_*` prefix is prone to spoofing and cannot be trusted! + -## Known Issues +== Known Issues == * We have temporarily disabled the data removal feature through plugin uninstallation, starting with version 3.9.3. We identified a few edge cases that did not behave as expected and we decided that a temporary removal is preferable at this time for such an impactful and irreversible operation. Our team is actively working on refining this feature to ensure it performs optimally and securely. We plan to reintroduce it in a future update with enhanced safeguards. -## Contribute +== Contribute == There are several ways you can get involved to help make Stream better: