feat: Validate event references in prev and auth events

Also threw in some lovely unit tests, as a treat.
This commit is contained in:
timedout authored and Ellis Git committed 2026-08-05 21:11:05 +00:00
1 parent d79ffe6ef0
commit 21ea9d3ea7
1 file changed
+127 -2
+127 -2
View File
@@ -1,5 +1,6 @@
use std::collections::HashMap;
use base64::Engine;
use conduwuit::{
Err, Event, EventTypeExt, PduEvent, Result, debug, debug::DebugInspect, debug_error,
debug_info, err, info, matrix::StateKey, state_res, trace,
@@ -7,14 +8,43 @@ use conduwuit::{
use futures::future::ready;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, ServerName,
api::error::ErrorKind, canonical_json::redact, events::StateEventType,
room_version_rules::RoomVersionRules,
api::error::ErrorKind,
canonical_json::redact,
events::StateEventType,
room_version_rules::{EventIdFormatVersion, RoomVersionRules},
};
use crate::rooms::{
event_handler::parse_incoming_pdu::expect_event_id_array, timeline::pdu_fits,
};
/// Checks that the given event ID matches the expected format by attempting to
/// decode the base64 string.
fn check_event_id_format(
event_id: &EventId,
room_version_rules: &RoomVersionRules,
) -> Result<Vec<u8>> {
let event_id_without_sigil = event_id
.as_str()
.strip_prefix("$")
.expect("event ID must start with a $ sigil");
let b64_alphabet = match room_version_rules.event_id_format {
| EventIdFormatVersion::V2 => base64::alphabet::STANDARD,
| EventIdFormatVersion::V3 => base64::alphabet::URL_SAFE,
| _ => return Err!("Unsupported event ID Format"),
};
let b64_engine = base64::engine::GeneralPurpose::new(
&b64_alphabet,
base64::engine::general_purpose::NO_PAD,
);
b64_engine.decode(event_id_without_sigil).map_err(|e| {
err!(Request(InvalidParam(debug_error!(
error=?e,
"PDU references an invalid event ID: {event_id}"
))))
})
}
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
@@ -42,6 +72,9 @@ impl super::Service {
if auth_events.len() > 10 {
return Err!(Request(BadJson("PDU has too many auth events")));
}
for auth_event_id in &auth_events {
check_event_id_format(auth_event_id, room_version_rules)?;
}
// The m.room.create event is the genesis event and has empty auth_events
// by definition, so it is exempt from the checks below requiring or
@@ -70,6 +103,9 @@ impl super::Service {
if prev_events.len() > 20 {
return Err!(Request(BadJson("PDU has too many prev events")));
}
for prev_event_id in &prev_events {
check_event_id_format(prev_event_id, room_version_rules)?;
}
Ok(())
}
@@ -306,3 +342,92 @@ impl super::Service {
Ok(true)
}
}
#[cfg(test)]
mod tests {
use ruma::server_name;
use super::*;
#[test]
fn v1_event_id_always_errors() {
let v1_event_id = EventId::new_v1(server_name!("example.com"));
assert!(
check_event_id_format(&v1_event_id, &RoomVersionRules::V3).is_err(),
"V1 event ID should not be valid in room V3"
);
assert!(
check_event_id_format(&v1_event_id, &RoomVersionRules::V4).is_err(),
"V1 event ID should not be valid in room V4"
);
}
#[test]
fn v2_event_id_ok_in_room_v3_only() {
let v2_event_id = EventId::new_v2_or_v3("KtY/RFXNXYxprwSOypvTlZsbohReRw19qcPATZDda4E")
.expect("fixture event hash must be valid");
let decoded_bytes = check_event_id_format(&v2_event_id, &RoomVersionRules::V3)
.expect("V2 event ID should be valid in room V3");
assert_eq!(
decoded_bytes,
vec![
42, 214, 63, 68, 85, 205, 93, 140, 105, 175, 4, 142, 202, 155, 211, 149, 155, 27,
162, 20, 94, 71, 13, 125, 169, 195, 192, 77, 144, 221, 107, 129
],
"V2 event reference hash did not decode to expected bytes"
);
assert!(
check_event_id_format(&v2_event_id, &RoomVersionRules::V4).is_err(),
"V2 event ID should not be valid in room V4"
);
}
#[test]
fn v3_event_id_errors_in_room_v3() {
let v3_event_id = EventId::new_v2_or_v3("zsj67_pqjr5qqh5GMTXqxLM0FqjP5OLrvXO0PjwWe88")
.expect("fixture event hash must be valid");
// Since the urlsafe replacements aren't in the standard base64 alphabet, this
// simply errors instead of decoding to potentially incorrect bytes.
assert!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V3).is_err(),
"V3 event ID should not be valid in room V3"
);
}
#[test]
fn v3_event_id_ok_in_room_v4_onward() {
let v3_event_id = EventId::new_v2_or_v3("zsj67_pqjr5qqh5GMTXqxLM0FqjP5OLrvXO0PjwWe88")
.expect("fixture event hash must be valid");
// Since the urlsafe replacements aren't in the standard base64 alphabet, this
// simply errors instead of decoding to potentially incorrect bytes.
let expected_bytes = vec![
206, 200, 250, 239, 250, 106, 142, 190, 106, 170, 30, 70, 49, 53, 234, 196, 179, 52,
22, 168, 207, 228, 226, 235, 189, 115, 180, 62, 60, 22, 123, 207,
];
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V4)
.expect("V3 event should be valid room V4"),
expected_bytes,
"V3 event ID in room V4 did not decode to expected bytes"
);
// These versions didn't change the algorithm, but might as well test them
// anyway
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V6)
.expect("V3 event should be valid room V6"),
expected_bytes,
"V3 event ID in room V6 did not decode to expected bytes"
);
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V10)
.expect("V3 event should be valid room V10"),
expected_bytes,
"V3 event ID in room V10 did not decode to expected bytes"
);
assert_eq!(
check_event_id_format(&v3_event_id, &RoomVersionRules::V12)
.expect("V3 event should be valid room V12"),
expected_bytes,
"V3 event ID in room V12 did not decode to expected bytes"
);
}
}