Skip to content

Status: validate the visitor IP address - #51349

Open
Nikschavan wants to merge 4 commits into
trunkfrom
stats-343-status-sanitize-visitor-ip
Open

Status: validate the visitor IP address#51349
Nikschavan wants to merge 4 commits into
trunkfrom
stats-343-status-sanitize-visitor-ip

Conversation

@Nikschavan

@Nikschavan Nikschavan commented Aug 18, 2026

Copy link
Copy Markdown
Member

Proposed changes

  • filter_var() with no filter argument applies FILTER_DEFAULT, which neither sanitizes nor validates. Visitor::get_ip() used it in both of its return paths, so whatever a header held was returned as-is.
  • Both paths now go through Automattic\Jetpack\IP\Utils::clean_ip(), which strips a port suffix, IPv6 brackets and the ::ffff: IPv4 mapping and then validates. A value that does not resolve to an address yields an empty string.
  • Forwarded headers are split on commas and the leftmost valid entry is returned, which is the client on a site behind a proxy or CDN. A header with no valid entry is skipped and the next one is tried.
  • automattic/jetpack-ip moves from require-dev to require in the status package. Modules::activate() has called IP_Utils::get_ip() since 2023, so this makes an existing runtime dependency explicit; every plugin shipping status already bundles ip except Beta, which now does.
  • The status changelog entry is minor, as the value existing callers receive changes.
  • The 19 composer.lock diffs are bookkeeping: each lock re-records the status package's require list. Only Beta's lock adds a package.

Flagged by the WordPress.org plugin review tooling while the standalone Jetpack Stats plugin was under review.

Related product discussion/links

  • WordPress.org plugin review of the standalone Jetpack Stats plugin, second automated round.

Does this pull request change what data or activity we track or use?

No new data is collected. Two behaviour changes for callers that record the visitor IP:

  • A value that is not an IP address — markup, free text, an out-of-range octet — now yields an empty string instead of the raw value.
  • A comma-separated forwarded header now yields its leftmost valid address instead of the raw list. Before this PR the whole list was returned verbatim, so the recorded value changes for sites behind a proxy, but it changes to the address the list was carrying.

Testing instructions

  • On a normal request, confirm ( new Visitor() )->get_ip() still returns the expected address.
  • Set $_SERVER['REMOTE_ADDR'] to a value that is not an IP address and confirm the method returns an empty string; set it to 1.2.3.4:8080 and confirm it returns 1.2.3.4.
  • Set $_SERVER['HTTP_X_FORWARDED_FOR'] to 5.6.7.8, 9.10.11.12 and call get_ip( true ); confirm it returns 5.6.7.8.
  • jp test php packages/status.

@Nikschavan Nikschavan self-assigned this Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the stats-343-status-sanitize-visitor-ip branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack stats-343-status-sanitize-visitor-ip
bin/jetpack-downloader test jetpack-mu-wpcom-plugin stats-343-status-sanitize-visitor-ip

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Beta plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: September 7, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Backup plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Boost plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Search plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Social plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Starter Plugin plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Protect plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Videopress plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Mu Wpcom plugin:

  • Next scheduled release: WordPress.com Simple releases happen semi-continuously (PCYsg-Jjm-p2)

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Inspect plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Wpcomsh plugin:

  • Next scheduled release: Atomic deploys happen twice daily on weekdays (p9o2xV-2EN-p2)

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Automattic For agencies client plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Classic Theme helper plugin plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Paypal Payment buttons plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Wpcloud Sso plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Premium Analytics plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Agents Manager plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Stats Data plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Aug 18, 2026
@jp-launch-control

jp-launch-control Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/packages/status/src/class-visitor.php 23/25 (92.00%) 2.00% 0 💚

Full summary · PHP report · JS report

filter_var() with no filter applies FILTER_DEFAULT, which does not sanitize. REMOTE_ADDR is now validated as an IP address, and the forwarded headers, which may hold lists, are sanitized as text.
@Nikschavan
Nikschavan force-pushed the stats-343-status-sanitize-visitor-ip branch from a5efbac to 1b5ff1f Compare August 18, 2026 07:38
@Nikschavan Nikschavan added [Status] Needs Review This PR is ready for review. and removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] In Progress labels Aug 18, 2026
@Nikschavan
Nikschavan requested a lite review from Copilot August 18, 2026 14:18
@Nikschavan
Nikschavan marked this pull request as ready for review August 18, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens visitor IP handling in the automattic/jetpack-status package by properly validating REMOTE_ADDR as an IP address and sanitizing forwarded header values as plain text (to allow legitimate comma-separated lists), addressing plugin review-tooling concerns.

Changes:

  • Sanitize forwarded IP-related headers using sanitize_text_field() instead of filter_var() default behavior.
  • Validate $_SERVER['REMOTE_ADDR'] with FILTER_VALIDATE_IP, returning an empty string when invalid.
  • Extend the PHPUnit bootstrap with a sanitize_text_field() polyfill and add tests covering the new behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
projects/packages/status/tests/php/Visitor_Test.php Adds new unit tests for Visitor::get_ip() validation/sanitization behavior.
projects/packages/status/tests/php/bootstrap.php Adds a sanitize_text_field() workalike for the test environment.
projects/packages/status/src/class-visitor.php Updates get_ip() to validate REMOTE_ADDR and sanitize forwarded headers.
projects/packages/status/changelog/stats-343-status-sanitize-visitor-ip Adds a changelog entry describing the behavioral change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread projects/packages/status/tests/php/bootstrap.php Outdated
Comment thread projects/packages/status/src/class-visitor.php Outdated
Comment thread projects/packages/status/tests/php/bootstrap.php Outdated
A header that does not hold a single address now falls through to the next one,
so a junk value in an earlier header cannot hide a valid address in a later one.
anomiex
anomiex previously approved these changes Aug 18, 2026

@anomiex anomiex left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks reasonable now. Hopefully nothing was depending on the broken behavior of returning non-IPs from X-Forwarded-For or the like 😉

@kraftbj kraftbj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's holding me up is the forwarded-header fall-through. A comma-separated X-Forwarded-For is the normal shape on any site behind a CDN or load balancer, and it now resolves to REMOTE_ADDR — the proxy — rather than the visitor. That's a live behavior change for self-hosted sites, it reaches WordPress.com through roughly twenty call sites, and it lands in every plugin bundling jetpack-status on the next release with no filter or option to opt out of it.

You flagged this yourself and said it needs its own change. I'd push back a little: the in-between state is worse than either end. Before, WordPress.com received a list it could parse. After, it receives a confidently wrong single address with no signal anything was dropped. IP_Utils::clean_ip() already handles the list, ports, brackets, and ::ffff: forms, so doing it here is small.

Two housekeeping items that apply either way: the plugins shipping this package need their own changelog entries (the package CHANGELOG never reaches users), and the description still describes the sanitize_text_field approach that 6b86c47 replaced.

I can push these myself if that's easier — the list handling, the plugin changelogs, and the description. Say the word and you can review the diff instead of writing it.

// @todo Some of these might actually be lists of IPs (e.g. HTTP_X_FORWARDED_FOR) or something else entirely (HTTP_VIA).
return filter_var( wp_unslash( $_SERVER[ $key ] ) );
// @todo Some of these might actually be lists of IPs (e.g. HTTP_X_FORWARDED_FOR) or something else entirely (HTTP_VIA). Those fail validation and fall through to the next header.
$ip = filter_var( wp_unslash( $_SERVER[ $key ] ), FILTER_VALIDATE_IP );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The realistic proxy case regresses here. Before this PR a comma-separated X-Forwarded-For was forwarded verbatim; now it fails validation, falls through the remaining headers, and lands on REMOTE_ADDR. On a self-hosted site behind its own proxy chain that's the load balancer, not the visitor, and it ships that way to WordPress.com from about twenty call sites — connection's proxy trait, stats, forms, scan, licensing, activity-log, plans, my-jetpack, and a dozen spots in core-rest-api-endpoints.

Automattic\Jetpack\IP\Utils::clean_ip() already does this work: projects/packages/ip/src/class-utils.php:59 handles the comma list, " unless " suffixes, IPv4:port, bracketed IPv6, and ::ffff: mapping, then validates. Minimal version without taking on the dependency:

foreach ( explode( ',', (string) wp_unslash( $_SERVER[ $key ] ) ) as $candidate ) {
	$ip = filter_var( trim( $candidate ), FILTER_VALIDATE_IP );
	if ( false !== $ip ) {
		return $ip;
	}
}

Leftmost-wins matches the order proxies append in. It's client-supplied and spoofable, but so was the entire raw header before this change, so it isn't a new trust boundary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Taken the clean_ip() route rather than the snippet, since every plugin that ships status already bundles ip except Beta, and Modules::activate() has called IP_Utils::get_ip() since 2023 — so this just makes an existing runtime dependency explicit. The loop still splits on commas here, as clean_ip() only handles one entry; leftmost valid wins. Pushed in 54c60de.

return filter_var( wp_unslash( $_SERVER[ $key ] ) );
// @todo Some of these might actually be lists of IPs (e.g. HTTP_X_FORWARDED_FOR) or something else entirely (HTTP_VIA). Those fail validation and fall through to the next header.
$ip = filter_var( wp_unslash( $_SERVER[ $key ] ), FILTER_VALIDATE_IP );
if ( false !== $ip ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the list case, a few single-address shapes that show up in the wild fail FILTER_VALIDATE_IP and now fall through: a port suffix (5.6.7.8:41234, which Azure App Service emits), bracketed IPv6 ([2001:db8::1]:443), ::ffff:-mapped IPv4, and plain leading whitespace.

Worth noting Jetpack now has two IP resolvers that disagree about the same request — IP_Utils::get_ip() normalizes all of those, this one rejects them. Routing through clean_ip() here would keep them from drifting further.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Covered by the same change — each candidate and REMOTE_ADDR go through clean_ip(). Tests for all three shapes plus leading whitespace. The two resolvers now agree on single-address parsing; what they still differ on is which headers they read, and that is a bigger question than this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This did increase the size of the PR due to bunch of composer.lock changes - all plugins already were shipping this automattic/jetpack-ip dependency though - this change does not introduce a new dependency

Comment thread projects/packages/status/src/class-visitor.php Outdated
Comment thread projects/packages/status/src/class-visitor.php Outdated
Comment thread projects/packages/status/tests/php/Visitor_Test.php Outdated
Comment thread projects/packages/status/changelog/stats-343-status-sanitize-visitor-ip Outdated
Comment thread projects/packages/status/changelog/stats-343-status-sanitize-visitor-ip Outdated
Comment thread projects/packages/status/src/class-visitor.php Outdated
@github-actions github-actions Bot added [Plugin] Backup A plugin that allows users to save every change and get back online quickly with one-click restores. [Plugin] Beta For serving live branches and the beta versions. https://github.com/automattic/jetpack-beta [Plugin] Boost A feature to speed up the site and improve performance. [Plugin] Classic Theme Helper Plugin [Plugin] Inspect [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] mu wpcom jetpack-mu-wpcom plugin [Plugin] Paypal Payment Buttons [Plugin] Premium Analytics [Plugin] Protect A plugin with features to protect a site: brute force protection, security scanning, and a WAF. [Plugin] Search A plugin to add an instant search modal to your site to help visitors find content faster. [Plugin] Social Issues about the Jetpack Social plugin [Plugin] Starter Plugin [Plugin] Stats Data [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. [Plugin] Wpcloud Sso [Plugin] Wpcomsh labels Aug 19, 2026
@Nikschavan

Copy link
Copy Markdown
Member Author

I can push these myself if that's easier

Please feel free to push to the PR if you have any suggestions 👍🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Status [Plugin] Agents Manager [Plugin] Automattic For Agencies Client [Plugin] Backup A plugin that allows users to save every change and get back online quickly with one-click restores. [Plugin] Beta For serving live branches and the beta versions. https://github.com/automattic/jetpack-beta [Plugin] Boost A feature to speed up the site and improve performance. [Plugin] Classic Theme Helper Plugin [Plugin] Inspect [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] mu wpcom jetpack-mu-wpcom plugin [Plugin] Paypal Payment Buttons [Plugin] Premium Analytics [Plugin] Protect A plugin with features to protect a site: brute force protection, security scanning, and a WAF. [Plugin] Search A plugin to add an instant search modal to your site to help visitors find content faster. [Plugin] Social Issues about the Jetpack Social plugin [Plugin] Starter Plugin [Plugin] Stats Data [Plugin] VideoPress A standalone plugin to add high-quality VideoPress videos to your site. [Plugin] Wpcloud Sso [Plugin] Wpcomsh [Status] In Progress [Status] Needs Review This PR is ready for review. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants