mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-18 21:52:30 -04:00
fix(crypto): Check the sender of an event matches owner of session
Having decrypted an event with a given megolm session, we need to check that the owner of that session actually matches the sender of an event, otherwise there is a danger of the sender being spoofed to make it look like it was sent by another user. Security-Impact: High CVE: CVE-2025-48937 GitHub-Advisory: GHSA-x958-rvg6-956w
This commit is contained in:
committed by
Damir Jelić
parent
7f3e144cb3
commit
13c1d20482
@@ -1540,8 +1540,30 @@ impl OlmMachine {
|
||||
) -> MegolmResult<(VerificationState, Option<OwnedDeviceId>)> {
|
||||
let sender_data = self.get_or_update_sender_data(session, sender).await?;
|
||||
|
||||
let (verification_state, device_id) =
|
||||
sender_data_to_verification_state(sender_data, session.has_been_imported());
|
||||
// If the user ID in the sender data doesn't match that in the event envelope,
|
||||
// this event is not from who it appears to be from.
|
||||
//
|
||||
// If `sender_data.user_id()` returns `None`, that means we don't have any
|
||||
// information about the owner of the session (i.e. we have
|
||||
// `SenderData::UnknownDevice`); in that case we fall through to the
|
||||
// logic in `sender_data_to_verification_state` which will pick an appropriate
|
||||
// `DeviceLinkProblem` for `VerificationLevel::None`.
|
||||
let (verification_state, device_id) = match sender_data.user_id() {
|
||||
Some(i) if i != sender => {
|
||||
// For backwards compatibility, we treat this the same as "Unknown device".
|
||||
// TODO: use a dedicated VerificationLevel here.
|
||||
(
|
||||
VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::MissingDevice,
|
||||
)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
Some(_) | None => {
|
||||
sender_data_to_verification_state(sender_data, session.has_been_imported())
|
||||
}
|
||||
};
|
||||
|
||||
Ok((verification_state, device_id))
|
||||
}
|
||||
|
||||
@@ -311,6 +311,69 @@ pub async fn mark_alice_identity_as_verified_test_helper(alice: &OlmMachine, bob
|
||||
.is_verified());
|
||||
}
|
||||
|
||||
/// Test that the verification state is set correctly when the sender of an
|
||||
/// event does not match the owner of the device that sent us the session.
|
||||
///
|
||||
/// In this test, Bob receives an event from Alice, but the HS admin has
|
||||
/// rewritten the `sender` of the event to look like another user.
|
||||
#[async_test]
|
||||
async fn test_verification_states_spoofed_sender() {
|
||||
let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper(
|
||||
tests::alice_id(),
|
||||
tests::user_id(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let room_id = room_id!("!test:example.org");
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let (event, _) = encrypt_message(&alice, room_id, &bob, "Secret message").await;
|
||||
bob.decrypt_room_event(&event, room_id, &decryption_settings)
|
||||
.await
|
||||
.expect("Bob could not decrypt event");
|
||||
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
|
||||
assert_matches!(
|
||||
event_encryption_info.verification_state,
|
||||
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
|
||||
);
|
||||
|
||||
// Alice now sends a second message to Bob, using the same room key, but the HS
|
||||
// admin rewrites the 'sender' to Charlie.
|
||||
let encrypted_content = alice
|
||||
.encrypt_room_event(
|
||||
room_id,
|
||||
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain(
|
||||
"spoofed message",
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let event = json!({
|
||||
"event_id": "$xxxxy:example.org",
|
||||
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
|
||||
"sender": "@charlie:example.org", // Note! spoofed sender
|
||||
"type": "m.room.encrypted",
|
||||
"content": encrypted_content,
|
||||
});
|
||||
let event = json_convert(&event).unwrap();
|
||||
|
||||
bob.decrypt_room_event(&event, room_id, &decryption_settings)
|
||||
.await
|
||||
.expect("Bob could not decrypt spoofed event");
|
||||
|
||||
// The verification_state of the event should be `MissingDevice` (since it
|
||||
// manifests as a message from Charlie which does not correspond to one of
|
||||
// Charlie's devices).
|
||||
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
|
||||
assert_matches!(
|
||||
event_encryption_info.verification_state,
|
||||
VerificationState::Unverified(VerificationLevel::None(DeviceLinkProblem::MissingDevice))
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_verification_states_multiple_device() {
|
||||
let (bob, _) = get_prepared_machine_test_helper(tests::user_id(), false).await;
|
||||
|
||||
@@ -252,6 +252,26 @@ impl SenderData {
|
||||
Self::SenderVerified { .. } => SenderDataType::SenderVerified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return our best guess of the owner of the associated megolm session.
|
||||
///
|
||||
/// For `SenderData::UnknownDevice`, we don't record any information about
|
||||
/// the owner of the sender, so returns `None`.
|
||||
pub(crate) fn user_id(&self) -> Option<OwnedUserId> {
|
||||
match &self {
|
||||
SenderData::UnknownDevice { .. } => None,
|
||||
SenderData::DeviceInfo { device_keys, .. } => Some(device_keys.user_id.clone()),
|
||||
SenderData::VerificationViolation(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
SenderData::SenderUnverified(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
SenderData::SenderVerified(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used when deserialising and the sender_data property is missing.
|
||||
|
||||
Reference in New Issue
Block a user