diff --git a/book/changelog.md b/book/changelog.md index 9614862f..2fcd48d6 100644 --- a/book/changelog.md +++ b/book/changelog.md @@ -56,6 +56,9 @@ This will be the first release of Grapevine since it was forked from Conduit 7. Only allow the admin bot to change the room ID that the admin room alias points to. ([!42](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/42)) +8. Fix vulnerability that allows a malicious server to trick a grapevine server + into signing arbitrary forged events via the send_invite endpoint. + ([!205](https://gitlab.computer.surgery/matrix/grapevine/-/merge_requests/205)) ### Removed diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 39b208f0..d003fb1d 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -51,7 +51,7 @@ use ruma::{ join_rules::{JoinRule, RoomJoinRulesEventContent}, member::{MembershipState, RoomMemberEventContent}, }, - StateEventType, TimelineEventType, + StateEventType, StaticEventContent, TimelineEventType, }, serde::{Base64, JsonObject, Raw}, state_res::Event, @@ -61,6 +61,7 @@ use ruma::{ OwnedServerSigningKeyId, OwnedSigningKeyId, OwnedUserId, RoomId, ServerName, Signatures, }; +use serde::Deserialize; use serde_json::value::{to_raw_value, RawValue as RawJsonValue}; use tokio::sync::RwLock; use tracing::{debug, error, field, trace, trace_span, warn}; @@ -1582,6 +1583,13 @@ async fn create_join_event( room_id: &RoomId, pdu: &RawJsonValue, ) -> Result { + #[derive(Deserialize)] + struct ExtractPdu<'a> { + #[serde(rename = "type")] + event_type: &'a str, + content: RoomMemberEventContent, + } + if !services().rooms.metadata.exists(room_id)? { return Err(Error::BadRequest( ErrorKind::NotFound, @@ -1591,6 +1599,28 @@ async fn create_join_event( services().rooms.event_handler.acl_check(sender_servername, room_id)?; + let extract: ExtractPdu<'_> = + serde_json::from_str(pdu.get()).map_err(|_| { + Error::BadRequest( + ErrorKind::InvalidParam, + "Event does not match expected schema", + ) + })?; + + if extract.event_type != RoomMemberEventContent::TYPE { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Event is not a membership event", + )); + } + if extract.content.membership != MembershipState::Join { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Not allowed to send a non-join membership event to send_join \ + endpoint.", + )); + } + // TODO: Grapevine does not implement restricted join rules yet, we always // reject let join_rules_event = services().rooms.state_accessor.room_state_get( @@ -1807,6 +1837,46 @@ pub(crate) async fn create_invite_route( ) })?; + let event_type = signed_event.get("type").ok_or_else(|| { + Error::BadRequest( + ErrorKind::InvalidParam, + "Event missing type property", + ) + })?; + if event_type.as_str() != Some(RoomMemberEventContent::TYPE) { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Event is not a membership event", + )); + } + + let content: RoomMemberEventContent = serde_json::from_value( + signed_event + .get("content") + .ok_or_else(|| { + Error::BadRequest( + ErrorKind::InvalidParam, + "Event missing content property", + ) + })? + .clone() + .into(), + ) + .map_err(|_| { + Error::BadRequest( + ErrorKind::InvalidParam, + "Event content is empty or invalid", + ) + })?; + + if content.membership != MembershipState::Invite { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Not allowed to send a non-invite membership event to invite \ + endpoint.", + )); + } + ruma::signatures::hash_and_sign_event( services().globals.server_name().as_str(), services().globals.keypair(),