PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
← All changes | includes/class-rest-manager.php +56 -0 1.3.7 → 1.4.1 View file →
@@ -160,8 +160,33 @@
160 160 return $result;
161 161 }
162 162
163 163 /**
164 + * Values that are shaped like a capability but cannot be one.
165 + *
166 + * WordPress's `__return_*` helpers are function names. Passed where a
167 + * capability belongs they are simply an unknown capability, and an unknown
168 + * capability is denied — so they close a route rather than open it. Listed
169 + * literally: the check has to be certain, because a capability that merely
170 + * happens to share a name with some function is legitimate.
171 + */
172 + private const NOT_A_CAPABILITY = array(
173 + '__return_true',
174 + '__return_false',
175 + '__return_zero',
176 + '__return_null',
177 + '__return_empty_array',
178 + '__return_empty_string',
179 + );
180 +
181 + /** Whether a declared capability is one `current_user_can()` could grant. */
182 + private static function is_capability( $capability ): bool {
183 + return is_string( $capability )
184 + && '' !== $capability
185 + && ! in_array( $capability, self::NOT_A_CAPABILITY, true );
186 + }
187 +
188 + /**
164 189 * Wrap permission_callback with the always-on cap check. A module may
165 190 * declare its own permission_callback for an extra-strict gate; both
166 191 * must pass.
167 192 *
@@ -180,8 +205,39 @@
180 205 private static function wrap_permission( Module $module, array $route ): callable {
181 206 $declared = $route['permission_callback'] ?? null;
182 207 $capability = $route['capability'] ?? 'manage_options';
183 208 $public = ! empty( $route['allow_unauthenticated'] );
209 +
210 + if ( ! $public && ! self::is_capability( $capability ) ) {
211 + /*
212 + * A route that reads as public and is closed to everyone.
213 + *
214 + * `'capability' => '__return_true'` is the shape this catches: a
215 + * function name, not a capability. `current_user_can()` denies an
216 + * unknown capability — for an anonymous caller AND for a logged-in
217 + * administrator — so the route answers 401 to every request while
218 + * looking, to the next person who reads it, like it lets everyone
219 + * through. One shipped that way, and what found it was a customer's
220 + * 401 rather than any test.
221 + *
222 + * It stays DENIED. Reading "public" out of a value that cannot be a
223 + * capability would turn a typo into an authentication bypass, which
224 + * is a far worse failure than the one being reported. The fix is to
225 + * say `'allow_unauthenticated' => true`, which is the only thing
226 + * that opens a route here, and this says so.
227 + */
228 + _doing_it_wrong(
229 + __METHOD__,
230 + esc_html(
231 + sprintf(
232 + 'Route "%s" declares "%s" as its capability. That is not a capability, so current_user_can() denies every caller, including administrators. Use \'allow_unauthenticated\' => true for a route that is meant to be public.',
233 + $module->slug() . ( $route['path'] ?? '' ),
234 + is_scalar( $capability ) ? (string) $capability : gettype( $capability )
235 + )
236 + ),
237 + 'xspeed 1.2.5'
238 + );
239 + }
184 240
185 241 return static function ( \WP_REST_Request $request ) use ( $declared, $capability, $public ) {
186 242 if ( ! $public && ! current_user_can( $capability ) ) {
187 243 return false;