fix: fail closed on permission() exceptions, harden RequestContext flag reset
- AbilityRegistrar: wrap permission_callback in try/catch(Throwable) like execute_callback, so a throwing permission() denies (Errors::internal()) instead of skipping rest_request_after_callbacks and leaving RequestContext's flag set for a later request in the same PHP worker. - RequestContext::recordRoute() now recomputes the flag (and bearer token) unconditionally on every matched REST request instead of only setting it true for our own route, so a stale true left behind by an earlier request is reset by the next non-matching one (fail closed, defense in depth). - RequestContext::recordRoute() matches the Authorization "Bearer" scheme case-insensitively. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -17,8 +17,11 @@ use WPMedia\MCP\OAuth\Auth\SecretManager;
|
||||
* `rest_request_before_callbacks` for every matched REST route before any
|
||||
* permission_callback runs and `rest_request_after_callbacks` right after the
|
||||
* route callback returned (WP_REST_Server::respond_to_request()). The flag is
|
||||
* set in the first and cleared in the second, so persistent PHP workers or a
|
||||
* later `rest_do_request()` in the same process never inherit it.
|
||||
* recomputed (not just set) in the first for every REST request – our route or
|
||||
* not – and cleared in the second, so persistent PHP workers or a later
|
||||
* `rest_do_request()` in the same process never inherit a stale "true", even
|
||||
* if `rest_request_after_callbacks` never fired for a previous OAuth server
|
||||
* request (e.g. a fatal error mid-dispatch).
|
||||
*/
|
||||
final class RequestContext
|
||||
{
|
||||
@@ -39,14 +42,13 @@ final class RequestContext
|
||||
*/
|
||||
public static function recordRoute(mixed $response, array $handler, WP_REST_Request $request): mixed
|
||||
{
|
||||
if ($request->get_route() === self::route()) {
|
||||
self::$isOAuthServerRequest = true;
|
||||
self::$clientName = null;
|
||||
$header = $request->get_header('authorization');
|
||||
self::$bearerToken = is_string($header) && str_starts_with($header, 'Bearer ')
|
||||
? substr($header, 7)
|
||||
: null;
|
||||
}
|
||||
// Recomputed on every matched REST request (not just ours) so a later,
|
||||
// unrelated request in the same persistent PHP worker can never inherit a
|
||||
// stale "true" left behind by an earlier OAuth server request whose
|
||||
// rest_request_after_callbacks somehow never ran (fail closed).
|
||||
self::$isOAuthServerRequest = $request->get_route() === self::route();
|
||||
self::$clientName = null;
|
||||
self::$bearerToken = self::$isOAuthServerRequest ? self::bearerToken($request) : null;
|
||||
|
||||
return $response;
|
||||
}
|
||||
@@ -120,4 +122,14 @@ final class RequestContext
|
||||
{
|
||||
return '/' . ServerRegistrar::NAMESPACE . '/' . ServerRegistrar::ROUTE;
|
||||
}
|
||||
|
||||
private static function bearerToken(WP_REST_Request $request): ?string
|
||||
{
|
||||
$header = $request->get_header('authorization');
|
||||
if (! is_string($header) || stripos($header, 'Bearer ') !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return substr($header, 7);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user