diff --git a/lib/SyTest/Federation/Server.pm b/lib/SyTest/Federation/Server.pm index ea15268e8..65e2e627b 100644 --- a/lib/SyTest/Federation/Server.pm +++ b/lib/SyTest/Federation/Server.pm @@ -411,6 +411,10 @@ __PACKAGE__->mk_await_request_pair( get_missing_events => [qw( :room_id )], ); +__PACKAGE__->mk_await_request_pair( + event_auth => [qw( :room_id :event_id )], +); + __PACKAGE__->mk_await_request_pair( backfill => [qw( :room_id )], ); diff --git a/tests/50federation/36-state.pl b/tests/50federation/36-state.pl index b84e9e4a3..28c477813 100644 --- a/tests/50federation/36-state.pl +++ b/tests/50federation/36-state.pl @@ -199,7 +199,7 @@ sub get_state_ids_from_server { # / # B (Y) # / \ / - # | X + # | X # \ / # C # @@ -685,3 +685,192 @@ sub get_state_ids_from_server { )->main::expect_m_not_found; }); }; + +test "Should not be able to take over the room by pretending there is no PL event", + # this test checks the situation fixed by + # https://github.com/matrix-org/synapse/pull/3397: in short, it used to be + # possible to take over a room by pretending that there was no power-levels + # event in the room. + # + # We're going to create a DAG that looks like this: + # + # A + # | + # B + # . + # . + # C + # | + # D + # | + # E + # + # Starting with a regular room, we send a message E, whose prev_event is D. + # We expect the remote server to request the missing events, so we send D, + # whose prev_event is C. + # + # The server will then request C, and the state at C. We give it a bogus + # state, which includes a PL event X which we make up and gives us all the + # power. + # + # The end state *should* be that X is rejected and the room state is + # unaffected. + + requires => [ + $main::OUTBOUND_CLIENT, $main::INBOUND_SERVER, $main::HOMESERVER_INFO[0], + # Create user and a publicly joinable room on the synapse. + local_user_and_room_fixtures(), + # Pick a user_id for our evil federation user. + federation_user_id_fixture() + ], + + do => sub { + my ( $outbound_client, $inbound_server, $info, $creator, $room_id, $evil_user_id ) = @_; + my $first_home_server = $info->server_name; + my $local_server_name = $outbound_client->server_name; + + # Join our evil user to the room. + $outbound_client->join_room( + server_name => $first_home_server, + room_id => $room_id, + user_id => $evil_user_id, + )->then( sub { + my ( $room ) = @_; + + # Fetch the create event and our evil user's join event. + my $create = $room->get_current_state_event("m.room.create"); + my $join = $room->get_current_state_event("m.room.member", $evil_user_id); + + my $evil_power_level_event_x = $room->create_event( + event_id_suffix => "pl_x", + + # Pick a depth of 0 so that this event apears before the + # real m.room.power_levels event when doing state resolution. + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/state/v1.py#L256 + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/state/v1.py#L301 + depth => 0, + # Refer to an event that doesn't exist so that synapse has to rely + # on the auth_events we supply to auth this event. + prev_events => [["\$this:event.does.not.exist", {}]], + type => "m.room.power_levels", + state_key => "", + sender => $evil_user_id, + content => { + users => { + # Give ourselves all the power in the room. + $evil_user_id => 100, + # Set the creator's power level to 0 so that the real + # m.room.power_levels event fails auth checks when compared + # to our power_level event. + $creator->user_id => 0, + }, + }, + ); + + my $evil_message_event_c = $room->create_event( + event_id_suffix => 'msg_c', + + # Pick a depth high enough to avoid the min_depth check. + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/handlers/federation.py#L245 + depth => 10, + # Reference an event that doesn't exist so that we can pick the + # state at this event. + prev_events => [["\$this:event.does.not.exist", {}]], + sender => $evil_user_id, + type => "m.room.message", + content => { + # Suitably evil laughter. + body => "hehehe...", + }, + ); + + my $msg_d = $room->create_event( + event_id_suffix => 'msg_d', + + depth => 11, + prev_events => [[$evil_message_event_c->{event_id}, {}]], + sender => $evil_user_id, + type => "m.room.message", + content => { + body => "totes legit", + }, + ); + + my $msg_e = $room->create_event( + event_id_suffix => 'msg_e', + depth => 11, + prev_events => [[$msg_d->{event_id}, {}]], + sender => $evil_user_id, + type => "m.room.message", + content => { + body => "nothing to see", + }, + ); + + Future->needs_all( + # Send the event using the federation send API. + $outbound_client->send_event( + event => $msg_e, + destination => $first_home_server, + ), + + # Synapse will request the missing events between the most recent + # event and the event we gave it. + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/handlers/federation.py#L266 + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/handlers/federation.py#L507 + $inbound_server->await_request_get_missing_events( $room_id )->then( sub { + my ( $req ) = @_; + + my $body = $req->body_from_json; + log_if_fail "/get_missing_events request", $body; + + assert_deeply_eq( + $body->{latest_events}, + [ $msg_e->{event_id } ], + "latest_events in /get_missing_events request", + ); + + # just return D + $req->respond_json( { + events => [ $msg_d ], + } ); + + Future->done(1); + }), + + # Synapse will ask us for the state at C. + # https://github.com/matrix-org/synapse/blob/v0.33.7/synapse/handlers/federation.py#L355 + $inbound_server->await_request_state_ids( $room_id, $evil_message_event_c->{event_id} )->then( sub { + my ( $req ) = @_; + $req->respond_json( { + # We tell it that the state is only our join event, the + # create event, and our evil power level event. + pdu_ids => [ + $create->{event_id}, + $join->{event_id}, + $evil_power_level_event_x->{event_id}, + ], + # We need to give it our join event so that the evil power + # level event passes the auth checks. + auth_chain_ids => [ + $create->{event_id}, + $join->{event_id}, + ], + }); + Future->done(1); + }), + ); + })->then( sub { + # Now check that our our evil power_level hasn't won the state resolution. + matrix_get_room_state( $creator, $room_id, + type => "m.room.power_levels", + state_key => "", + ); + })->then( sub { + my ( $content ) = @_; + + assert_eq( $content->{users}{$creator->user_id}, 100 ); + + Future->done(1); + }); + };