From 995726923bd8de4d3d019af8abb1a7e9e58fa6f0 Mon Sep 17 00:00:00 2001 From: timedout Date: Sat, 11 Jul 2026 06:09:42 +0100 Subject: [PATCH] feat: Enforce new federation invite checks These were introduced in spec v1.16 however we didn't implement them until now for compatibility. --- src/api/server/invite.rs | 127 +++++++++++++++--- src/service/rooms/event_handler/pdu_checks.rs | 6 +- 2 files changed, 113 insertions(+), 20 deletions(-) diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index 9d2fab0e3..078511ec7 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -1,19 +1,24 @@ +use std::collections::{HashMap, hash_map::Entry}; + use axum::extract::State; use axum_client_ip::ClientIp; use base64::{Engine as _, engine::general_purpose}; use conduwuit::{ - Err, Error, PduEvent, Result, err, error, - matrix::{Event, event::gen_event_id}, + Err, Error, EventTypeExt, PduEvent, Result, err, error, + matrix::{Event, StateKey, event::gen_event_id}, utils::{self, hash::sha256}, warn, }; use ruma::{ - CanonicalJsonValue, UserId, + CanonicalJsonValue, OwnedEventId, UserId, api::{ error::{ErrorKind, IncompatibleRoomVersionErrorData}, federation::membership::{RawStrippedState, create_invite}, }, - events::room::member::{MembershipState, RoomMemberEventContent}, + events::{ + StateEventType, + room::member::{MembershipState, RoomMemberEventContent}, + }, serde::JsonObject, }; @@ -67,6 +72,14 @@ pub(crate) async fn create_invite_route( let mut signed_event = utils::to_canonical_object(&body.event) .map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?; + services + .server_keys + .verify_event(&signed_event, &room_version_rules) + .await + .map_err(|e| { + err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) + })?; + // Ensure this is a membership event if signed_event .get("type") @@ -123,23 +136,83 @@ pub(crate) async fn create_invite_route( return Err!(Request(InvalidParam("User does not belong to this homeserver."))); } - // Make sure we're not ACL'ed from their room. - services - .rooms - .event_handler - .acl_check(recipient_user.server_name(), &body.room_id) - .await?; + // Validate the invite room state. + let mut invite_state_map: HashMap<(StateEventType, StateKey), _> = + HashMap::with_capacity(body.invite_room_state.len()); + let mut create_event_id: Option = None; + for (idx, invite_state_event) in body.invite_room_state.iter().cloned().enumerate() { + // Stripped state hasn't been sent over federation since v1.16. + let RawStrippedState::Pdu(raw_pdu) = invite_state_event else { + return Err!(Request(InvalidParam( + "PDU in invite state (index {idx}) violates the room event format" + ))); + }; + let (state_event_room_id, state_event_id, state_event_json) = services + .rooms + .event_handler + .parse_incoming_pdu(&raw_pdu) + .await + .map_err(|e| err!(Request(InvalidParam("Invalid PDU in invite state: {e}"))))?; - services - .server_keys - .hash_and_sign_event(&mut signed_event, &room_version_rules) - .map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?; + if state_event_room_id != body.room_id { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) belongs to the wrong room" + ))); + } - // Generate event id - let event_id = gen_event_id(&signed_event, &room_version_rules)?; + services + .server_keys + .verify_event(&signed_event, &room_version_rules) + .await + .map_err(|e| { + err!(Request(InvalidParam("Signature verification failed on invite event: {e}"))) + })?; - // Add event_id back - signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string())); + let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) is not a state event" + ))); + }; + let Some(event_type) = state_event_json.get("event_type").and_then(|k| k.as_str()) else { + return Err!(Request(InvalidParam( + "PDU in invite state ({state_event_id}) is not an event?" + ))); + }; + + let key = StateEventType::from(event_type).with_state_key(state_key); + match invite_state_map.entry(key) { + | Entry::Occupied(entry) => + return Err!(Request(InvalidParam( + "Duplicate state events in invite state for state key: {:?}", + entry.key(), + ))), + | Entry::Vacant(entry) => { + if entry.key().0 == StateEventType::RoomCreate { + let event_id = gen_event_id(&state_event_json, &room_version_rules) + .expect("must be able to generate room ID"); + create_event_id = Some(event_id); + } + entry.insert(state_event_json); + }, + } + } + let Some(create_event_id) = create_event_id else { + return Err!(Request(InvalidParam( + "Invite state does not contain the m.room.create event" + ))); + }; + invite_state_map.iter().try_for_each(|(key, event_json)| { + service::rooms::event_handler::Service::pdu_format_check_1( + event_json, + &room_version_rules, + &create_event_id, + ) + .map_err(|e| { + err!(Request(InvalidParam( + "PDU in invite state for {key:?} violates the room event format: {e}" + ))) + }) + })?; if services.rooms.metadata.is_banned(&body.room_id).await && !services.users.is_admin(&recipient_user).await @@ -161,6 +234,24 @@ pub(crate) async fn create_invite_route( return Err!(Request(Forbidden("Invite rejected by antispam service."))); } + // Make sure we're not ACL'ed from their room. + services + .rooms + .event_handler + .acl_check(recipient_user.server_name(), &body.room_id) + .await?; + + services + .server_keys + .hash_and_sign_event(&mut signed_event, &room_version_rules) + .map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?; + + // Generate event id + let event_id = gen_event_id(&signed_event, &room_version_rules)?; + + // Add event_id back + signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string())); + let mut invite_state = body.invite_room_state.clone(); let mut event: JsonObject = serde_json::from_str(body.event.get()) diff --git a/src/service/rooms/event_handler/pdu_checks.rs b/src/service/rooms/event_handler/pdu_checks.rs index 1288b97a9..d258f834d 100644 --- a/src/service/rooms/event_handler/pdu_checks.rs +++ b/src/service/rooms/event_handler/pdu_checks.rs @@ -18,7 +18,7 @@ impl super::Service { /// Checks that the PDU conforms to the PDU format (check 1). This is /// already mostly done during deserialisation, so this function just checks /// that the PDU isn't a too large. - pub(super) fn pdu_format_check_1( + pub fn pdu_format_check_1( pdu_json: &CanonicalJsonObject, room_version_rules: &RoomVersionRules, create_event_id: &EventId, @@ -45,6 +45,8 @@ impl super::Service { let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id); if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events { return Err!(Request(BadJson("PDU references a create event"))); + } else if event_format.allow_room_create_in_auth_events && !create_event_in_auth_events { + return Err!(Request(BadJson("PDU does not reference the room create event"))); } let prev_events = expect_event_id_array(pdu_json, "prev_events")?; @@ -59,7 +61,7 @@ impl super::Service { /// the content hash verification fails (check 3), returning the /// potentially modified JSON. Returns an error if the PDU cannot be /// redacted, or fails signature verification. - pub(super) async fn signature_hash_check_2_3( + pub async fn signature_hash_check_2_3( &self, pdu_json: CanonicalJsonObject, room_version_rules: &RoomVersionRules,