updated plugin ActivityPub version 9.1.0

This commit is contained in:
2026-07-28 15:03:10 +00:00
committed by Gitium
parent bf428f0e45
commit ff806e1811
217 changed files with 6098 additions and 3025 deletions

View File

@ -31,7 +31,7 @@ class Server {
// Schedule cleanup cron.
if ( ! \wp_next_scheduled( 'activitypub_oauth_cleanup' ) ) {
\wp_schedule_event( time(), 'daily', 'activitypub_oauth_cleanup' );
\wp_schedule_event( \time(), 'daily', 'activitypub_oauth_cleanup' );
}
\add_action( 'activitypub_oauth_cleanup', array( self::class, 'cleanup' ) );
}
@ -56,6 +56,22 @@ class Server {
return $result;
}
/*
* Only honor OAuth bearer tokens on the plugin's own REST API.
*
* These are scoped ActivityPub C2S grants. Honoring them on core routes
* (e.g. POST /wp/v2/posts) would establish a full-capability session that
* ignores the token's granted scope, since scope is only enforced where
* the plugin opts in via check_oauth_permission() (CWE-863). Every C2S
* endpoint lives under ACTIVITYPUB_REST_NAMESPACE, so restricting here
* does not affect legitimate clients. Direct callers (outbox permalinks,
* SSE query-param auth) invoke this method outside REST dispatch and are
* trusted to have scoped the context themselves.
*/
if ( ! self::is_authenticatable_request() ) {
return $result;
}
$validated = Token::validate( $token );
if ( \is_wp_error( $validated ) ) {
@ -68,6 +84,63 @@ class Server {
return true;
}
/**
* Whether the current request may be authenticated with an OAuth bearer token.
*
* Bearer tokens are scoped ActivityPub C2S grants and must only authenticate
* the plugin's own REST API. During REST dispatch we therefore require the
* requested route to live under {@see ACTIVITYPUB_REST_NAMESPACE}; a token
* presented to a core route such as `/wp/v2/posts` is ignored so its granted
* scope cannot be bypassed. Outside REST dispatch (e.g. outbox permalinks
* that authenticate explicitly) there is no route to scope against, so the
* request is allowed through to the caller's own checks.
*
* @since 9.1.0
*
* @return bool True if the request may be OAuth-authenticated.
*/
private static function is_authenticatable_request() {
global $wp;
$route = isset( $wp->query_vars['rest_route'] ) ? (string) $wp->query_vars['rest_route'] : '';
if ( '' === $route ) {
// No REST route in context: only trust non-REST (direct/permalink) callers.
return ! \wp_is_serving_rest_request();
}
$route = '/' . \ltrim( $route, '/' );
$namespace = '/' . \trim( ACTIVITYPUB_REST_NAMESPACE, '/' );
return $route === $namespace || 0 === \strpos( $route, $namespace . '/' );
}
/**
* Reject requests that authenticated with an OAuth bearer token.
*
* OAuth bearer tokens are scoped ActivityPub C2S grants and must not reach
* the plugin's admin and management endpoints, which authorize by WordPress
* capability rather than by OAuth scope. Without this guard a token consented
* to for a narrow scope (e.g. read) could drive privileged, capability-gated
* actions such as moderation. Cookie-authenticated admin-UI requests do not
* establish an OAuth session and are unaffected.
*
* @since 9.1.0
*
* @return \WP_Error|null WP_Error when the request is OAuth-authenticated, null otherwise.
*/
public static function deny_if_oauth() {
if ( ! self::is_oauth_request() ) {
return null;
}
return new \WP_Error(
'activitypub_oauth_not_allowed',
\__( 'OAuth authentication is not allowed for this endpoint.', 'activitypub' ),
array( 'status' => 403 )
);
}
/**
* Get the current OAuth token from the request.
*
@ -113,11 +186,11 @@ class Server {
}
// Check for Bearer token.
if ( 0 !== strpos( $auth_header, 'Bearer ' ) ) {
if ( 0 !== \strpos( $auth_header, 'Bearer ' ) ) {
return null;
}
return substr( $auth_header, 7 );
return \substr( $auth_header, 7 );
}
/**
@ -142,14 +215,14 @@ class Server {
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Fallback: read from Apache's own header API (case-insensitive).
if ( ! function_exists( 'apache_request_headers' ) ) {
if ( ! \function_exists( 'apache_request_headers' ) ) {
return null;
}
$headers = apache_request_headers();
$headers = \apache_request_headers();
foreach ( $headers as $key => $value ) {
if ( 'authorization' === strtolower( $key ) ) {
if ( 'authorization' === \strtolower( $key ) ) {
return $value;
}
}
@ -214,7 +287,7 @@ class Server {
return new \WP_Error(
'activitypub_insufficient_scope',
/* translators: %s: The required scope */
sprintf( \__( 'This action requires the "%s" scope.', 'activitypub' ), $scope ),
\sprintf( \__( 'This action requires the "%s" scope.', 'activitypub' ), $scope ),
array( 'status' => 403 )
);
}
@ -248,7 +321,7 @@ class Server {
'revocation_endpoint' => $base_url . 'oauth/revoke',
'introspection_endpoint' => $base_url . 'oauth/introspect',
'registration_endpoint' => $base_url . 'oauth/clients',
'scopes_supported' => Scope::ALL,
'scopes_supported' => Scope::supported(),
'response_types_supported' => array( 'code' ),
'response_modes_supported' => array( 'query' ),
'grant_types_supported' => array( 'authorization_code', 'refresh_token' ),
@ -337,7 +410,7 @@ class Server {
// Build form action URL.
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$form_url = \add_query_arg(
array_merge( array( 'action' => 'activitypub_authorize' ), $authorize_params ),
\array_merge( array( 'action' => 'activitypub_authorize' ), $authorize_params ),
\wp_login_url()
);
@ -448,7 +521,7 @@ class Server {
$url = Sanitize::redirect_uri( \add_query_arg( $params, $redirect_uri ) );
\nocache_headers();
header( 'Location: ' . $url, true, 303 );
\header( 'Location: ' . $url, true, 303 );
exit;
}
}