Skip to content

Commit e7b7844

Browse files
committed
fix: Use the DisplayName struct to protect against homoglyph attacks
1 parent 563d649 commit e7b7844

File tree

12 files changed

+318
-81
lines changed

12 files changed

+318
-81
lines changed

crates/matrix-sdk-base/src/client.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
#[cfg(feature = "e2e-encryption")]
1717
use std::sync::Arc;
1818
use std::{
19-
collections::{BTreeMap, BTreeSet},
19+
collections::{BTreeMap, BTreeSet, HashMap},
2020
fmt, iter,
2121
ops::Deref,
2222
};
@@ -70,7 +70,7 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat
7070
#[cfg(feature = "e2e-encryption")]
7171
use crate::RoomMemberships;
7272
use crate::{
73-
deserialized_responses::{RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
73+
deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent},
7474
error::{Error, Result},
7575
event_cache_store::EventCacheStoreLock,
7676
response_processors::AccountDataProcessor,
@@ -1332,7 +1332,7 @@ impl BaseClient {
13321332
#[cfg(feature = "e2e-encryption")]
13331333
let mut user_ids = BTreeSet::new();
13341334

1335-
let mut ambiguity_map: BTreeMap<String, BTreeSet<OwnedUserId>> = BTreeMap::new();
1335+
let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
13361336

13371337
for raw_event in &response.chunk {
13381338
let member = match raw_event.deserialize() {
@@ -1363,7 +1363,11 @@ impl BaseClient {
13631363

13641364
if let StateEvent::Original(e) = &member {
13651365
if let Some(d) = &e.content.displayname {
1366-
ambiguity_map.entry(d.clone()).or_default().insert(member.state_key().clone());
1366+
let display_name = DisplayName::new(d);
1367+
ambiguity_map
1368+
.entry(display_name)
1369+
.or_default()
1370+
.insert(member.state_key().clone());
13671371
}
13681372
}
13691373

crates/matrix-sdk-base/src/deserialized_responses.rs

+6-4
Original file line numberDiff line numberDiff line change
@@ -446,10 +446,12 @@ impl MemberEvent {
446446
///
447447
/// It there is no `displayname` in the event's content, the localpart or
448448
/// the user ID is returned.
449-
pub fn display_name(&self) -> &str {
450-
self.original_content()
451-
.and_then(|c| c.displayname.as_deref())
452-
.unwrap_or_else(|| self.user_id().localpart())
449+
pub fn display_name(&self) -> DisplayName {
450+
DisplayName::new(
451+
self.original_content()
452+
.and_then(|c| c.displayname.as_deref())
453+
.unwrap_or_else(|| self.user_id().localpart()),
454+
)
453455
}
454456
}
455457

crates/matrix-sdk-base/src/rooms/members.rs

+8-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use std::{
16-
collections::{BTreeMap, BTreeSet},
16+
collections::{BTreeSet, HashMap},
1717
sync::Arc,
1818
};
1919

@@ -30,7 +30,8 @@ use ruma::{
3030
};
3131

3232
use crate::{
33-
deserialized_responses::{MemberEvent, SyncOrStrippedState},
33+
deserialized_responses::{DisplayName, MemberEvent, SyncOrStrippedState},
34+
store::ambiguity_map::is_display_name_ambiguous,
3435
MinimalRoomMemberEvent,
3536
};
3637

@@ -67,8 +68,10 @@ impl RoomMember {
6768
} = room_info;
6869

6970
let is_room_creator = room_creator.as_deref() == Some(event.user_id());
70-
let display_name_ambiguous =
71-
users_display_names.get(event.display_name()).is_some_and(|s| s.len() > 1);
71+
let display_name = event.display_name();
72+
let display_name_ambiguous = users_display_names
73+
.get(&display_name)
74+
.is_some_and(|s| is_display_name_ambiguous(&display_name, s));
7275
let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
7376

7477
Self {
@@ -245,6 +248,6 @@ pub(crate) struct MemberRoomInfo<'a> {
245248
pub(crate) power_levels: Arc<Option<SyncOrStrippedState<RoomPowerLevelsEventContent>>>,
246249
pub(crate) max_power_level: i64,
247250
pub(crate) room_creator: Option<OwnedUserId>,
248-
pub(crate) users_display_names: BTreeMap<&'a str, BTreeSet<OwnedUserId>>,
251+
pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
249252
pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
250253
}

crates/matrix-sdk-base/src/rooms/normal.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ use super::{
6767
#[cfg(feature = "experimental-sliding-sync")]
6868
use crate::latest_event::LatestEvent;
6969
use crate::{
70-
deserialized_responses::{MemberEvent, RawSyncOrStrippedState},
70+
deserialized_responses::{DisplayName, MemberEvent, RawSyncOrStrippedState},
7171
notification_settings::RoomNotificationMode,
7272
read_receipts::RoomReadReceipts,
7373
store::{DynStateStore, Result as StoreResult, StateStoreExt},
@@ -819,8 +819,7 @@ impl Room {
819819
})
820820
.collect::<BTreeMap<_, _>>();
821821

822-
let display_names =
823-
member_events.iter().map(|e| e.display_name().to_owned()).collect::<Vec<_>>();
822+
let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
824823
let room_info = self.member_room_info(&display_names).await?;
825824

826825
let mut members = Vec::new();
@@ -900,7 +899,7 @@ impl Room {
900899

901900
let profile = self.store.get_profile(self.room_id(), user_id).await?;
902901

903-
let display_names = [event.display_name().to_owned()];
902+
let display_names = [event.display_name()];
904903
let room_info = self.member_room_info(&display_names).await?;
905904

906905
Ok(Some(RoomMember::from_parts(event, profile, presence, &room_info)))
@@ -911,7 +910,7 @@ impl Room {
911910
/// Async because it can read from storage.
912911
async fn member_room_info<'a>(
913912
&self,
914-
display_names: &'a [String],
913+
display_names: &'a [DisplayName],
915914
) -> StoreResult<MemberRoomInfo<'a>> {
916915
let max_power_level = self.max_power_level();
917916
let room_creator = self.inner.read().creator().map(ToOwned::to_owned);

crates/matrix-sdk-base/src/sliding_sync/mod.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,10 @@ async fn cache_latest_events(
695695
changes: Option<&StateChanges>,
696696
store: Option<&Store>,
697697
) {
698+
use crate::{
699+
deserialized_responses::DisplayName, store::ambiguity_map::is_display_name_ambiguous,
700+
};
701+
698702
let mut encrypted_events =
699703
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
700704

@@ -752,11 +756,13 @@ async fn cache_latest_events(
752756
.as_original()
753757
.and_then(|profile| profile.content.displayname.as_ref())
754758
.and_then(|display_name| {
759+
let display_name = DisplayName::new(&display_name);
760+
755761
changes.ambiguity_maps.get(room.room_id()).and_then(
756762
|map_for_room| {
757-
map_for_room
758-
.get(display_name)
759-
.map(|user_ids| user_ids.len() > 1)
763+
map_for_room.get(&display_name).map(|users| {
764+
is_display_name_ambiguous(&display_name, users)
765+
})
760766
},
761767
)
762768
});

0 commit comments

Comments
 (0)