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:
@@ -113,6 +113,28 @@ final class AbilityRegistrarTest extends TestCase
|
||||
self::assertSame(['ok' => true], $result);
|
||||
}
|
||||
|
||||
public function testPermissionExceptionBecomesNeutralInternalError(): void
|
||||
{
|
||||
Functions\when('current_user_can')->justReturn(true);
|
||||
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
||||
$log = (string) tempnam(sys_get_temp_dir(), 'lit-mcp-log');
|
||||
$previous = ini_set('error_log', $log);
|
||||
$ability = $this->ability();
|
||||
$ability->expects('permission')->andThrow(new RuntimeException('secret path /var/www/x.php'));
|
||||
|
||||
try {
|
||||
$result = ($this->registrarFor($ability)->argsFor($ability)['permission_callback'])([]);
|
||||
} finally {
|
||||
ini_set('error_log', (string) $previous);
|
||||
}
|
||||
|
||||
self::assertInstanceOf(WP_Error::class, $result);
|
||||
self::assertSame('lit_internal_error', $result->get_error_code());
|
||||
self::assertStringNotContainsString('/var/www', $result->get_error_message());
|
||||
self::assertStringContainsString('lit/test failed', (string) file_get_contents($log));
|
||||
unlink($log);
|
||||
}
|
||||
|
||||
public function testExceptionBecomesNeutralInternalError(): void
|
||||
{
|
||||
// error_log() is a PHP internal and cannot be stubbed; redirect it to a temp file instead.
|
||||
|
||||
@@ -40,6 +40,18 @@ final class RequestContextTest extends TestCase
|
||||
self::assertFalse(RequestContext::isOAuthServerRequest());
|
||||
}
|
||||
|
||||
public function testRecordRouteResetsAStaleFlagOnALaterNonMatchingRequest(): void
|
||||
{
|
||||
// Simulates rest_request_after_callbacks never having fired for a
|
||||
// previous OAuth server request (e.g. a fatal error mid-dispatch),
|
||||
// leaving the flag orphaned as true in a persistent PHP worker.
|
||||
RequestContext::reset(true);
|
||||
|
||||
RequestContext::recordRoute(null, [], new WP_REST_Request('/wp/v2/users/me'));
|
||||
|
||||
self::assertFalse(RequestContext::isOAuthServerRequest());
|
||||
}
|
||||
|
||||
public function testRecordRouteReturnsResponseUnchanged(): void
|
||||
{
|
||||
$response = new stdClass();
|
||||
@@ -87,6 +99,25 @@ final class RequestContextTest extends TestCase
|
||||
self::assertSame('https://claude.ai/oauth/mcp-oauth-client-metadata', RequestContext::clientName());
|
||||
}
|
||||
|
||||
public function testClientNameMatchesTheBearerSchemeCaseInsensitively(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn('test-secret');
|
||||
$token = JWT::encode([
|
||||
'sub' => '3',
|
||||
'app_pass_id' => 'uuid-unknown',
|
||||
'client_id' => 'https://claude.ai/oauth/mcp-oauth-client-metadata',
|
||||
'exp' => time() + 60,
|
||||
], 'test-secret');
|
||||
|
||||
RequestContext::recordRoute(
|
||||
null,
|
||||
[],
|
||||
new WP_REST_Request(self::ROUTE, ['authorization' => 'bearer ' . $token])
|
||||
);
|
||||
|
||||
self::assertSame('https://claude.ai/oauth/mcp-oauth-client-metadata', RequestContext::clientName());
|
||||
}
|
||||
|
||||
public function testClientNameIsEmptyForAnInvalidToken(): void
|
||||
{
|
||||
Functions\when('get_option')->justReturn('test-secret');
|
||||
|
||||
Reference in New Issue
Block a user