Page profiler endpoint fatals on PHP 8 when the request has no `b` (background selectors) field
维护者通常 1 天内回复
@girishpanchal30 已经在做这个了。
开始于 2026年10月29日。
评估
调研方向
从 inc/rest.php 第 958-994 行附近开始,这里会读取 b 参数并传给 count(),并检查第 142-160 行附近的路由注册。用提供的 REST 请求或纯 PHP 检查来复现,然后让缺失、为 null 或字符串类型的 b 表现为没有后台选择器,而不触发 fatal。还要覆盖 :987 和 :993 处的嵌套计数。完成意味着这些情况正常返回或返回标准错误响应,而不是 HTTP 500,并且有效请求仍会存储优化数据。
由索引模型根据 Issue 内容生成。
描述
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
- 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() ); ' - 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. - 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 forrepro-profile-id). - Over HTTP as a logged-out client: open a not-yet-profiled page, copy
pageProfileId,_t,hmacandpageProfileUrlfrom theoptimoleDataOptimizerscript in the page source, and POST them asu,t,h,pu(plusd,a) to/wp-json/optml/v1/optimizationswithContent-Type: application/jsonandOrigin: https://<site host>, leaving outb. The response is HTTP 500 with"code":"internal_server_error"and the fatal above is logged. - Pure PHP check of the same lines (PHP 8.4.19), body decoded with
json_decode( $body, true )asWP_REST_Requestdoes: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.phpis the same onmasteranddevelopmentat 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.
- 主要语言
- PHP
- 星标
- 74
- 派生
- 14
- 平均合并
- 1 天 22 小时
- 30 天内合并 PR
- 29
环境准备
- 提供 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
Codeinwp/optimole-wp 的其他 Issue
-
Background image with a gradient is not lazy-loaded on WordPress 7.1 (Quote, Pullquote, Verse, Post Content, Accordion)可能已有人在做 @girishpanchal30 于 2 天前认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 78/100
Codeinwp/optimole-wp#1203 · 3 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
Page profiler never treats the detected LCP image as LCP, so it gets no fetchpriority="high" (stored ID is a string, compared with === to an int)可能已有人在做 @girishpanchal30 于 2 天前认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 88/100
Codeinwp/optimole-wp#1202 · 3 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
Account sign-up fatals when the Optimole API returns an empty or non-JSON response可能已有人在做 @girishpanchal30 于 2 天前认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 85/100
Codeinwp/optimole-wp#1200 · 3 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
Renaming an image leaves the old URL in comments and term descriptions, with "Unknown column 'ID'" database errors可能已有人在做 @girishpanchal30 于 2 天前认领。 未关闭bug
难度 2/5 1-3 小时 新手友好度 74/100
Codeinwp/optimole-wp#1199 · 3 条评论 · 已指派 1 人 ·
维护者通常 1 天内回复
-
bug-report customer report
难度 4/5 3-5 天 新手友好度 56/100
Codeinwp/optimole-wp#1211 ·
维护者通常 1 天内回复
查看 Codeinwp/optimole-wp 的全部 Issue
相似的 Issue
-
bug webbackend
难度 2/5 1-3 小时 新手友好度 74/100
DOMjudge/domjudge#3886 · 1 条评论 ·
维护者通常 1 天内回复
-
bug
难度 2/5 1-3 小时 新手友好度 65/100
opensourcepos/opensourcepos#4794 ·
维护者通常 1 天内回复
-
repo:cms
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 65/100
维护者通常 1 天内回复
-
REST API: decimal ids like 1.5 give 500 on folder and report endpoints可能已有人在做 @console-log-life 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 65/100
维护者通常 3 天内回复