validate event type and membership for create_join and create_invite

Both of these endpoints sign the received event so without the
validation a malicious server can use these endpoints to trick our
server into signing effectively arbitrary forged events from local
users.

Rebased from a continuwuity patch by nex. The create_join changes were
not present in the continuwuity version because these checks were
already present there.

Co-authored-by: Olivia Lee <olivia@computer.surgery>
This commit is contained in:
timedout 2025-12-20 21:35:18 -08:00 committed by Olivia Lee
parent c4abca1eb5
commit 9a50c2448a
2 changed files with 74 additions and 1 deletions

View file

@ -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<create_join_event::v2::RoomState> {
#[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(),