Improve Validation (#847)

* initial

* re add authorisation request

* remove unnecessary checks

* validate object

* these checks are done by the inbox now

* only handle activitypub requests
This commit is contained in:
Matthias Pfefferle 2024-09-16 16:57:33 +02:00 committed by GitHub
parent 2729f2f0e1
commit 36610f5d75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 82 deletions

View file

@ -34,14 +34,6 @@ class Announce {
* @return void
*/
public static function handle_announce( $array, $user_id, $activity = null ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.arrayFound
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
if ( ! isset( $array['object'] ) ) {
return;
}
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
// @todo maybe send email

View file

@ -21,6 +21,13 @@ class Create {
10,
3
);
\add_filter(
'activitypub_validate_object',
array( self::class, 'validate_object' ),
10,
3
);
}
/**
@ -33,17 +40,6 @@ class Create {
* @return void
*/
public static function handle_create( $array, $user_id, $object = null ) {
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
if (
! isset( $array['object'] ) ||
! isset( $array['object']['id'] )
) {
return;
}
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
// @todo maybe send email
@ -67,4 +63,38 @@ class Create {
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
}
/**
* Validate the object
*
* @param bool $valid The validation state
* @param string $param The object parameter
* @param \WP_REST_Request $request The request object
* @param array $array The activity-object
*
* @return bool The validation state: true if valid, false if not
*/
public static function validate_object( $valid, $param, $request ) {
$json_params = $request->get_json_params();
if (
'Create' !== $json_params['type'] ||
is_wp_error( $request )
) {
return $valid;
}
$object = $json_params['object'];
$required = array(
'id',
'inReplyTo',
'content',
);
if ( array_intersect( $required, array_keys( $object ) ) !== $required ) {
return false;
}
return $valid;
}
}

View file

@ -191,10 +191,6 @@ class Inbox {
public static function user_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['user_id'] = array(
'required' => true,
'type' => 'string',
@ -207,6 +203,7 @@ class Inbox {
$params['actor'] = array(
'required' => true,
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
@ -214,15 +211,13 @@ class Inbox {
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$params['object'] = array(
'required' => true,
'validate_callback' => function ( $param, $request, $key ) {
return apply_filters( 'activitypub_validate_object', true, $param, $request, $key );
},
);
return $params;
@ -234,42 +229,11 @@ class Inbox {
* @return array list of parameters
*/
public static function shared_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['id'] = array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
);
$params['actor'] = array(
'required' => true,
//'type' => array( 'object', 'string' ),
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
);
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$params['object'] = array(
'required' => true,
//'type' => 'object',
);
$params = self::user_inbox_post_parameters();
$params['to'] = array(
'required' => false,
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );
@ -280,6 +244,7 @@ class Inbox {
);
$params['cc'] = array(
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );
@ -290,6 +255,7 @@ class Inbox {
);
$params['bcc'] = array(
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );

View file

@ -21,6 +21,7 @@ class Server {
public static function init() {
self::register_routes();
\add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_activitypub_requests' ), 9, 3 );
\add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
}
@ -77,6 +78,10 @@ class Server {
return $response;
}
if ( \is_wp_error( $response ) ) {
return $response;
}
$route = $request->get_route();
// check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
@ -124,4 +129,51 @@ class Server {
return $response;
}
/**
* Callback function to validate incoming ActivityPub requests
*
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*
* @return mixed|WP_Error The response, error, or modified response.
*/
public static function validate_activitypub_requests( $response, $handler, $request ) {
if ( 'HEAD' === $request->get_method() ) {
return $response;
}
$route = $request->get_route();
if (
\is_wp_error( $response ) ||
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
) {
return $response;
}
$params = $request->get_json_params();
// Type is required for ActivityPub requests, so it fail later in the process
if ( ! isset( $params['type'] ) ) {
return $response;
}
if (
ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
) {
return new WP_Error(
'activitypub_server_does_not_accept_incoming_interactions',
\__( 'This server does not accept incoming interactions.', 'activitypub' ),
// We have to use a 2XX status code here, because otherwise the response will be
// treated as an error and Mastodon might block this WordPress instance.
array( 'status' => 202 )
);
}
return $response;
}
}

View file

@ -52,24 +52,10 @@ class Test_Activitypub_Create_Handler extends WP_UnitTestCase {
);
}
public function test_handle_create_object_unset_rejected() {
$object = $this->create_test_object();
unset( $object['object'] );
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
public function test_handle_create_non_public_rejected() {
$object = $this->create_test_object();
$object['cc'] = [];
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
public function test_handle_create_no_id_rejected() {
$object = $this->create_test_object();
unset( $object['object']['id'] );
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
$this->assertNull( $converted );
}
}

View file

@ -109,13 +109,6 @@ class Test_Activitypub_Interactions extends WP_UnitTestCase {
$this->assertEquals( 'Helloexampleexample', $comment['comment_content'] );
}
public function test_convert_object_to_comment_not_reply_rejected() {
$object = $this->create_test_object();
unset( $object['object']['inReplyTo'] );
$converted = Activitypub\Collection\Interactions::add_comment( $object );
$this->assertFalse( $converted );
}
public function test_convert_object_to_comment_already_exists_rejected() {
$object = $this->create_test_object( 'https://example.com/test_convert_object_to_comment_already_exists_rejected' );
Activitypub\Collection\Interactions::add_comment( $object );