Make WordPress Core

Opened 20 hours ago

Last modified 3 hours ago

#65839 new defect (bug)

Site Health REST API test can fail with rest_cookie_invalid_nonce when full browser cookie jar is sent

Reported by: hamishwright Owned by:
Priority: normal Milestone:
Component: Site Health Version: 7.0.3
Severity: minor Keywords: rest-api false-positive site-health has-patch has-unit-tests
Cc: Focuses: tests, rest-api

Description (last modified by hamishwright)

WordPress Site Health can falsely report the REST API as failing with:

REST API Endpoint: https://example.com/wp-json/wp/v2/types/post?context=edit
REST API Response: (403) Forbidden

The REST response body is:

{"code":"rest_cookie_invalid_nonce","message":"Cookie check failed","data":{"status":403}}

In WP_Site_Health::get_test_rest_availability(), core builds the loopback request using:

<?php
$cookies = wp_unslash( $_COOKIE );
$r = wp_remote_get( $url, compact( 'cookies', 'headers', 'timeout', 'sslverify' ) );

This sends the entire browser cookie jar to the REST endpoint, not just WordPress authentication cookies.

On the affected site, a diagnostic confirmed:

WordPress auth cookies only -> 200
WP_Http_Cookie objects only -> 200
Explicit Cookie header only -> 200
Full $_COOKIE jar, as Site Health -> 403 rest_cookie_invalid_nonce

The full cookie jar included unrelated cookies from analytics, LiteSpeed, Cloudflare, WooCommerce, Sourcebuster, Jetpack, wp-settings-*, and others. When the outgoing Site Health REST loopback request was filtered to include only:

wordpress_logged_in_*
wordpress_sec_*
wordpress_test_cookie

the Site Health REST API warning cleared immediately.

Expected behavior:

Site Health’s authenticated REST loopback test should only send the WordPress cookies required for authentication, or otherwise avoid allowing unrelated browser cookies to interfere with REST nonce/cookie validation.

Actual behavior:

Site Health passes all of $_COOKIE into the loopback request.A sufficiently large or otherwise problematic browser cookie jar can cause the REST API availability test to return a false 403, even though the REST endpoint and WordPress authentication cookies are working correctly.

Suggested fix:

In WP_Site_Health::get_test_rest_availability(), filter $cookies before passing them to wp_remote_get() so only WordPress authentication/test cookies are sent.

Change History (2)

#1 @hamishwright
20 hours ago

  • Description modified (diff)

This ticket was mentioned in PR #12952 on WordPress/wordpress-develop by @khokansardar.


3 hours ago
#2

  • Keywords has-patch has-unit-tests added

Restricts the cookies sent with the Site Health REST API availability test to the cookie names WordPress itself defines, instead of forwarding the visitor's entire browser cookie jar to the loopback request.

What the problem was:

  • WP_Site_Health::get_test_rest_availability() passed wp_unslash( $_COOKIE ) straight to wp_remote_get(), so every cookie in the visitor's browser — analytics, CDN, cache, commerce, and others — was forwarded to the site's own REST endpoint.
  • None of those cookies play any part in authenticating the request, which is done with the WordPress cookies plus the X-WP-Nonce header.
  • On the reporting site this produced a false (403) Forbidden with rest_cookie_invalid_nonce, while the same request limited to the WordPress cookies returned 200.

What the fix does:

  • Filters $cookies through array_intersect_key() so only AUTH_COOKIE, SECURE_AUTH_COOKIE, LOGGED_IN_COOKIE, and TEST_COOKIE are forwarded.
  • Adds a test asserting that unrelated cookies are dropped and the WordPress cookies are kept.

Approach and why:

  • The allowlist uses core's cookie constants rather than a wordpress_ name prefix. A prefix would keep wp-settings-*, which is not an authentication cookie, and would break on sites that redefine the cookie constants in wp-config.php. The test covers the wp-settings-* case specifically.
  • This narrows what is sent, so it is worth stating plainly: a site whose edge or host requires some other cookie to reach the endpoint (for example Cloudflare Access) can restore it through the existing http_request_args filter, which runs after this code. No new filter is introduced.
  • Only the method named in the ticket is changed. WP_Site_Health::can_perform_loopback(), WP_Automatic_Updater, and wp-admin/includes/file.php use the same wp_unslash( $_COOKIE ) pattern for different loopback targets and need their own analysis; they are left for a separate ticket.

Testing instructions:

  1. Log in as an administrator and add several unrelated cookies for the site in the browser (for example _ga, wp-settings-time-1, and a large dummy cookie of a few kilobytes).
  1. Visit Tools > Site Health. On trunk, inspect the outgoing loopback request and confirm the whole cookie jar is sent; on a site sensitive to this the REST API test reports (403) Forbidden with rest_cookie_invalid_nonce.
  1. Apply the patch, reload Site Health, and confirm the REST API test reports "The REST API is available" and that only the WordPress cookies are sent.
  1. Run phpunit --filter test_get_test_rest_availability_sends_only_wordpress_cookies tests/phpunit/tests/admin/wpSiteHealth.php. The test fails on trunk and passes with the patch.

## Use of AI Tools

AI assistance: Yes

Tool(s): Claude Code

Model(s): Claude Opus 5

Used for: Ticket analysis, code implementation, and tests. All changes were reviewed and validated by me.

---

This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

Note: See TracTickets for help on using tickets.