Page profiler endpoint fatals on PHP 8 when the request has no `b` (background selectors) field

Offen Anfängerfreundlich
#1,204 4 Kommentare 0 Reaktionen 1 zugewiesene Person Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

@girishpanchal30 arbeitet bereits daran.

Seit 29.10.2026.

  • #1195 von @pirate-bot — offen

Bewertung

Schwierigkeit
2/5
Geschätzter Aufwand
1-3 Stunden
Anfängerfreundlichkeit
88/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
php, wordpress
Bereich
api, backend

Rechercherichtung

Beginne in inc/rest.php im Bereich der Zeilen 958-994, wo der Parameter b gelesen und an count() übergeben wird, und prüfe die Routenregistrierung im Bereich der Zeilen 142-160. Reproduziere das mit der bereitgestellten REST-Anfrage oder dem reinen PHP-Check und sorge dann dafür, dass ein fehlendes, null- oder stringförmiges b sich wie keine Background-Selektoren verhält, ohne einen Fatal auszulösen. Decke außerdem die verschachtelten Zählungen bei :987 und :993 ab. Erledigt bedeutet, dass diese Fälle normal oder mit der Standardfehlerantwort zurückkehren, nicht mit HTTP 500, und dass eine gültige Anfrage weiterhin Optimierungsdaten speichert.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

bug
Description

POST /wp-json/optml/v1/optimizations (the public page profiler endpoint) reads b with get_param( 'b' ) and calls count() on it without checking its type. b is not declared in the route args, so when it is missing (or null, or a string) PHP 8 throws TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given at inc/rest.php:974. WordPress then answers HTTP 500 "There has been a critical error on this website." and a PHP fatal is written to the error log.

This happens after the HMAC and Origin checks, so it needs a valid page token. That token is printed in the page source for every logged-out visitor on a page that is not profiled yet, so any HTTP client (not only the bundled script) can trigger the fatal and the log line. The bundled script always sends b, so normal visits are not affected.

The nested values have the same problem: count( $above_fold_bg_selectors ) (:987) and count( $bg_urls ) (:993) fatal on null. The bundled script sets each entry to null first and only replaces it when it finds URLs (assets/js/modules/main.js:231-237), so a null there is likely rare but possible.

Expected: a missing or malformed b is treated as "no background selectors" (or rejected with the normal code: error response), never a fatal.

Step-by-step reproduction instructions
  1. On a test site running PHP 8.x with Optimole 4.2.17 active (the route is registered even when the plugin is not connected), run:
    wp eval '
    $u  = "repro-profile-id"; $t = time(); $pu = home_url( "/" );
    $r  = new WP_REST_Request( "POST", "/optml/v1/optimizations" );
    $r->set_header( "content-type", "application/json" );
    $r->set_header( "origin", "https://" . wp_parse_url( home_url(), PHP_URL_HOST ) );
    $r->set_body( wp_json_encode( [ "d" => 1, "a" => [ 1 ], "u" => $u, "t" => $t, "pu" => $pu, "h" => wp_hash( $u . $t . $pu, "nonce" ) ] ) );
    var_dump( rest_do_request( $r )->get_data() );
    '
    
  2. Result (from reading the code and the PHP check in step 5; I did not run it on a live site): PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in .../inc/rest.php:974.
  3. Add "b" => new stdClass() to the body and run it again: it should get past :974 with no fatal and return "Optimization data stored successfully" (it stores a small profile transient for repro-profile-id).
  4. Over HTTP as a logged-out client: open a not-yet-profiled page, copy pageProfileId, _t, hmac and pageProfileUrl from the optimoleDataOptimizer script in the page source, and POST them as u, t, h, pu (plus d, a) to /wp-json/optml/v1/optimizations with Content-Type: application/json and Origin: https://<site host>, leaving out b. The response is HTTP 500 with "code":"internal_server_error" and the fatal above is logged.
  5. Pure PHP check of the same lines (PHP 8.4.19), body decoded with json_decode( $body, true ) as WP_REST_Request does:
    b absent           => TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given
    b is {}            => Optimization data stored successfully
    b null             => TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given
    b string           => TypeError: count(): Argument #1 ($value) must be of type Countable|array, string given
    b inner url null   => TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given   (the :993 count)
    
Screenshots, screen recording, code snippet or Help Scout ticket

b is read after the signature and origin checks and counted without a type check:
https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/rest.php#L958-L976

if ( wp_hash( $url . $time . $current_url, 'nonce' ) !== $hmac ) { ... }
$bg_selectors = $request->get_param( 'b' );
...
if ( count( $bg_selectors ) > 100 ) {

Nested counts: https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/rest.php#L983-L994

The route only declares d, a and u, with permission_callback => '__return_true':
https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/rest.php#L142-L160

The token is printed for logged-out visitors on unprofiled pages:
https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/manager.php#L515-L523 and https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/admin.php#L1070-L1083

The bundled script's null placeholder for nested entries:
https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/assets/js/modules/main.js#L226-L237

Core does not catch it: call_user_func( $handler['callback'], $request ) in WP_REST_Server::respond_to_request() (wp-includes/rest-api/class-wp-rest-server.php:1293 in WordPress 7.1.3, :1181 in 6.2). The fatal error handler then answers with status 500 (wp-includes/class-wp-fatal-error-handler.php:212 in 7.1.3). REST is not a protected endpoint, so no recovery mode email is sent (wp-includes/class-wp-recovery-mode.php:176-178).

A likely fix is to declare b in the route args (type => object, default => []) or to guard with is_array() before each count() and foreach, at :974, :987 and :993.

Environment info
  • Optimole 4.2.17 (inc/rest.php is the same on master and development at 63f3f7c)
  • Any WordPress version the plugin supports (5.5 and later; core lines cited from 7.1.3 and 6.2)
  • PHP 8.0 and later. On PHP 7.4 count( null ) only logs a warning and the request goes on.
Is the issue you are reporting a regression

No. The same code has been there since the endpoint was added in 4.0.0.

Found while writing the WordPress compatibility checks for Optimole.

Vorherrschende Sprache
PHP
Sterne
74
Forks
14
Ø Merge
1 T. 23 Std.
Gemergte PRs (30 T.)
31

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus Codeinwp/optimole-wp

Alle Issues in Codeinwp/optimole-wp

Ähnliche Issues

Weitere Issues zu PHP

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.