Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve client IP resolution for Stream logs #1459

Merged
merged 23 commits into from
Oct 24, 2023
Merged

Conversation

kasparsd
Copy link
Contributor

@kasparsd kasparsd commented Oct 9, 2023

Fixes #1456.

  • Rely on $_SERVER['REMOTE_ADDR'] as the canonical source for the request origin IP.
  • Show a notice in admin if $_SERVER['REMOTE_ADDR'] is not specified.
  • Fallback to $_SERVER['HTTP_X_FORWARDED_FOR'] only if $_SERVER['REMOTE_ADDR'] not present.

Checklist

  • Project documentation has been updated to reflect the changes in this pull request, if applicable.
  • I have tested the changes in the local development environment (see contributing.md).
  • I have added phpunit tests.

Release Changelog

  • Fix: Prefer $_SERVER['REMOTE_ADDR'] for the source of user IP and fallback to $_SERVER['HTTP_X_FORWARDED_FOR'] only in case REMOTE_ADDR is not available. Introduce a wp_stream_client_ip_address filter to adjust the trusted client IP.

if ( is_admin() && Alerts::POST_TYPE === $screen->post_type ) {
return true;
if ( is_admin() && function_exists( 'get_current_screen' ) ) {
$screen = get_current_screen();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Account for calls to this method when get_current_screen() is not available.


// Fallback to unsafe IP extracted from the request HTTP headers.
if ( empty( $ip_address ) ) {
$ip_address = $this->plugin->get_unsafe_client_ip_address();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep the previous behaviour for now. Consider removing this fallback in the future releases (as mentioned in the changelog).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why you opted to do two separate calls in here, as opposed to having the fallback be part of get_client_ip_address() instead.

I thought the latter would have been easier for removing the fallback behavior at a later date. Also, it would allow get_unsafe_client_ip_address() to be private instead so we don't create an unwanted public interface for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason is we want to check just the "reliable" IP address in some instances with no fallbacks (when showing the admin notice, for example). This also clearly identifies the places where the use of get_unsafe_client_ip_address() should be wrapped into a conditional eventually.

@@ -174,12 +167,6 @@ public function is_record_excluded( $connector, $context, $action, $user = null,
$user = wp_get_current_user();
}

if ( is_null( $ip ) ) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We expect all of the data arriving here to be sanitized and checked already.

@@ -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 );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the main plugin class responsible for knowing properties of the current request. Expose the knowledge via a helper method.

@kasparsd kasparsd requested a review from schlessera October 9, 2023 13:56
Comment on lines 348 to 349
'HTTP_X_FORWARDED_FOR',
'HTTP_FORWARDED_FOR',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 7239 proposes a new standard 'HTTP_FORWARDED': https://datatracker.ietf.org/doc/html/rfc7239

It is still a comma-separated list, but contains prefixes in front of the IP address and other directives, like so:

Forwarded: for=192.0.2.43,for=198.51.100.17;by=203.0.113.60;proto=http;host=example.com

Not sure it's worthwhile adding support for that upcoming standard as of yet, though, as it doesn't seem to be widely supported => https://c960657.github.io/forwarded.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's leave this to a separate pull request.

classes/class-admin.php Outdated Show resolved Hide resolved
$header_client_ip = $header_client_ips[0];
}

$client_ip = wp_stream_filter_var( trim( $header_client_ip ), FILTER_VALIDATE_IP );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for future reference:
The known drawbacks of FILTER_VALIDATE_IP can be ignored here, as it goes through a special polyfill and ends up being implemented via WP_Http::is_ip_address() instead of via PHP's internal filter_var.

readme.txt Outdated Show resolved Hide resolved
readme.txt Outdated
Comment on lines 80 to 94
`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;
}
);`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note 100% sure about the readme.txt markdown compatibility, but shouldn't this section use triple backtick notation instead?

/**
* IP address for the current request to be associated with the log entry.
*
* @var null|false|string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intention behind having three different types here?

From what I can gather in the PR here, we have:

  • valid IP address gets returned as a string
  • invalid IP address gets returned as a false
  • no IP address gets returned as null

As mixed types always offer extra surface for bugs, just covering the bases here:

  1. Are we distinguishing between these three types in other parts of the code? I.e., does an invalid IP address trigger anything differently than a missing IP address?
  2. Are these three types properly stored in the database?
  3. Are these three types properly rendered in all UI dialogs and reports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just mirroring the output types of filter_input(). This is the canonical state of the REMOTE_ADDR value and the methods can use these values as needed.

classes/class-log.php Show resolved Hide resolved
$ip_address = $this->plugin->get_client_ip_address();

// Fallback to unsafe IP extracted from the request HTTP headers.
if ( empty( $ip_address ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty( $ip_address ) will be true for both the case where no IP address was provided as well for when an invalid IP address is provided. Is that the intended behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit tricky here -- we're attempting to preserve the HTTP_X_FORWARDED_FOR usage from before but now it will only happen if REMOTE_ADDR is not configured which might not be the case making this an equally breaking change.

I'm thinking we should probably do a breaking change release and make it consistent immediately to avoid confusion.


// Fallback to unsafe IP extracted from the request HTTP headers.
if ( empty( $ip_address ) ) {
$ip_address = $this->plugin->get_unsafe_client_ip_address();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why you opted to do two separate calls in here, as opposed to having the fallback be part of get_client_ip_address() instead.

I thought the latter would have been easier for removing the fallback behavior at a later date. Also, it would allow get_unsafe_client_ip_address() to be private instead so we don't create an unwanted public interface for this.

*
* @return string|null
*/
public function get_unsafe_client_ip_address() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned elsewhere, if this fallback behavior would be called immediately while initially setting $this->client_ip_address, we wouldn't need to make it public and not have it part of the public interface.

@kasparsd
Copy link
Contributor Author

@schlessera I feel like attempting the fallback behaviour might not be in our best interest here because it would actually be different from the previous behaviour. Let's make it a breaking change instead (does this warrant a 4.0.0 release?) and encourage everyone to use the newly introduced filter for adjusting the client IP address on environments where either REMOTE_ADDR is reporting wrong.

@calvinalkan
Copy link

@schlessera I feel like attempting the fallback behaviour might not be in our best interest here because it would actually be different from the previous behaviour. Let's make it a breaking change instead (does this warrant a 4.0.0 release?) and encourage everyone to use the newly introduced filter for adjusting the client IP address on environments where either REMOTE_ADDR is reporting wrong.

My 2C.

  1. The fallback behaviour is insecure. People wil shoot themselves in the foot.
  2. If PHP can't trust it's server to set REMOTE_ADDR correctly, or at all, all hope is lost. This should not be a consideration.
  3. Yes, breaking change, 4.0.0

@schlessera schlessera force-pushed the add/event-source-ip branch 2 times, most recently from 178177e to b42c4c0 Compare October 16, 2023 16:46
@schlessera schlessera merged commit 7fcbb08 into develop Oct 24, 2023
2 of 3 checks passed
@schlessera schlessera deleted the add/event-source-ip branch October 24, 2023 12:23
@schlessera schlessera added this to the 4.0.0 milestone Oct 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve event source IP reporting
3 participants