add some ACL paw-gun checks, better PUT state event validation

Signed-off-by: June Clementine Strawberry <june@3.dog>
This commit is contained in:
June Clementine Strawberry 2025-03-07 00:57:39 -05:00
parent 2c58a6efda
commit 4f882c3bd8
No known key found for this signature in database
2 changed files with 178 additions and 98 deletions

View file

@ -1,7 +1,7 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Error, Result, debug, err, info, result::NotFound, utils}; use conduwuit::{Err, Error, Result, debug, debug_warn, err, info, result::NotFound, utils};
use futures::{StreamExt, stream::FuturesUnordered}; use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{ use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId, OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
@ -41,6 +41,20 @@ pub(crate) async fn upload_keys_route(
let (sender_user, sender_device) = body.sender(); let (sender_user, sender_device) = body.sender();
for (key_id, one_time_key) in &body.one_time_keys { for (key_id, one_time_key) in &body.one_time_keys {
if one_time_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
?key_id,
?one_time_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
)
})
.is_err()
{
continue;
}
services services
.users .users
.add_one_time_key(sender_user, sender_device, key_id, one_time_key) .add_one_time_key(sender_user, sender_device, key_id, one_time_key)
@ -48,7 +62,12 @@ pub(crate) async fn upload_keys_route(
} }
if let Some(device_keys) = &body.device_keys { if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize()?; let deser_device_keys = device_keys.deserialize().map_err(|e| {
err!(Request(BadJson(debug_warn!(
?device_keys,
"Invalid device keys JSON uploaded by client: {e}"
))))
})?;
if deser_device_keys.user_id != sender_user { if deser_device_keys.user_id != sender_user {
return Err!(Request(Unknown( return Err!(Request(Unknown(

View file

@ -11,6 +11,7 @@ use ruma::{
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
server_acl::RoomServerAclEventContent,
}, },
}, },
serde::Raw, serde::Raw,
@ -194,134 +195,194 @@ async fn allowed_to_send_state_event(
) -> Result { ) -> Result {
match event_type { match event_type {
| StateEventType::RoomCreate => { | StateEventType::RoomCreate => {
return Err!(Request(BadJson( return Err!(Request(BadJson(debug_warn!(
?room_id,
"You cannot update m.room.create after a room has been created." "You cannot update m.room.create after a room has been created."
))); ))));
},
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
if acl_content.allow.is_empty() {
return Err!(Request(BadJson(debug_warn!(
?room_id,
"Sending an ACL event with an empty allow key will permanently \
brick the room for non-conduwuit's as this equates to no servers \
being allowed to participate in this room."
))));
}
if acl_content.deny.contains(&String::from("*"))
&& acl_content.allow.contains(&String::from("*"))
{
return Err!(Request(BadJson(debug_warn!(
?room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
permanently brick the room for non-conduwuit's as this equates to \
no servers being allowed to participate in this room."
))));
}
if acl_content.deny.contains(&String::from("*"))
&& !acl_content.is_allowed(services.globals.server_name())
{
return Err!(Request(BadJson(debug_warn!(
?room_id,
"Sending an ACL event with a deny key value of \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
if !acl_content.allow.contains(&String::from("*"))
&& !acl_content.is_allowed(services.globals.server_name())
{
return Err!(Request(BadJson(debug_warn!(
?room_id,
"Sending an ACL event for an allow key without \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room server ACL event is invalid: {e}"
))));
},
};
}, },
// Forbid m.room.encryption if encryption is disabled
| StateEventType::RoomEncryption => | StateEventType::RoomEncryption =>
if !services.globals.allow_encryption() { // Forbid m.room.encryption if encryption is disabled
if !services.config.allow_encryption {
return Err!(Request(Forbidden("Encryption is disabled on this homeserver."))); return Err!(Request(Forbidden("Encryption is disabled on this homeserver.")));
}, },
// admin room is a sensitive room, it should not ever be made public
| StateEventType::RoomJoinRules => { | StateEventType::RoomJoinRules => {
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await { if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id { if admin_room_id == room_id {
if let Ok(join_rule) = match json.deserialize_as::<RoomJoinRulesEventContent>() {
serde_json::from_str::<RoomJoinRulesEventContent>(json.json().get()) | Ok(join_rule) =>
{ if join_rule.join_rule == JoinRule::Public {
if join_rule.join_rule == JoinRule::Public { return Err!(Request(Forbidden(
return Err!(Request(Forbidden( "Admin room is a sensitive room, it cannot be made public"
"Admin room is a sensitive room, it cannot be made public" )));
))); },
} | Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room join rules event is invalid: {e}"
))));
},
} }
} }
} }
}, },
// admin room is a sensitive room, it should not ever be made world readable
| StateEventType::RoomHistoryVisibility => { | StateEventType::RoomHistoryVisibility => {
if let Ok(visibility_content) = // admin room is a sensitive room, it should not ever be made world readable
serde_json::from_str::<RoomHistoryVisibilityEventContent>(json.json().get()) if let Ok(admin_room_id) = services.admin.get_admin_room().await {
{ match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
if let Ok(admin_room_id) = services.admin.get_admin_room().await { | Ok(visibility_content) => {
if admin_room_id == room_id if admin_room_id == room_id
&& visibility_content.history_visibility && visibility_content.history_visibility
== HistoryVisibility::WorldReadable == HistoryVisibility::WorldReadable
{ {
return Err!(Request(Forbidden( return Err!(Request(Forbidden(
"Admin room is a sensitive room, it cannot be made world readable \ "Admin room is a sensitive room, it cannot be made world \
(public room history)." readable (public room history)."
))); )));
} }
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room history visibility event is invalid: {e}"
))));
},
} }
} }
}, },
| StateEventType::RoomCanonicalAlias => { | StateEventType::RoomCanonicalAlias => {
if let Ok(canonical_alias) = match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
serde_json::from_str::<RoomCanonicalAliasEventContent>(json.json().get()) | Ok(canonical_alias_content) => {
{ let mut aliases = canonical_alias_content.alt_aliases.clone();
let mut aliases = canonical_alias.alt_aliases.clone();
if let Some(alias) = canonical_alias.alias { if let Some(alias) = canonical_alias_content.alias {
aliases.push(alias); aliases.push(alias);
} }
for alias in aliases { for alias in aliases {
if !services.globals.server_is_ours(alias.server_name()) { let (alias_room_id, _servers) =
return Err!(Request(Forbidden( services.rooms.alias.resolve_alias(&alias, None).await?;
"canonical_alias must be for this server"
if alias_room_id != room_id {
return Err!(Request(Forbidden(
"Room alias {alias} does not belong to room {room_id}"
)));
}
}
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room canonical alias event is invalid: {e}"
))));
},
}
},
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(membership_content) => {
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if services
.rooms
.state_cache
.is_joined(state_key, room_id)
.await
{
return Err!(Request(InvalidParam(
"{state_key} is already joined, an authorising user is not required."
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
))); )));
} }
if !services if !services
.rooms .rooms
.alias .state_cache
.resolve_local_alias(&alias) .is_joined(&authorising_user, room_id)
.await .await
.is_ok_and(|room| room == room_id)
// Make sure it's the right room
{ {
return Err!(Request(Forbidden( return Err!(Request(InvalidParam(
"You are only allowed to send canonical_alias events when its \ "Authorising user {authorising_user} is not in the room, they \
aliases already exist" cannot authorise the join."
))); )));
} }
} }
} },
}, | Err(e) => {
| StateEventType::RoomMember => {
let Ok(membership_content) =
serde_json::from_str::<RoomMemberEventContent>(json.json().get())
else {
return Err!(Request(BadJson( return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \ "Membership content must have a valid JSON body with at least a valid \
membership state." membership state: {e}"
))); )));
}; },
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) = membership_content.join_authorized_via_users_server {
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if services
.rooms
.state_cache
.is_joined(state_key, room_id)
.await
{
return Err!(Request(InvalidParam(
"{state_key} is already joined, an authorising user is not required."
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this homeserver"
)));
}
if !services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they cannot \
authorise the join."
)));
}
}
}, },
| _ => (), | _ => (),
} }