diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml index 92a5b7c4..55b303b2 100644 --- a/.forgejo/workflows/release-image.yml +++ b/.forgejo/workflows/release-image.yml @@ -180,7 +180,7 @@ jobs: file: "docker/Dockerfile" build-args: | GIT_COMMIT_HASH=${{ github.sha }}) - GIT_COMMIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }}) + GIT_COMMIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }} GIT_REMOTE_URL=${{github.event.repository.html_url }} GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }} platforms: ${{ matrix.platform }} diff --git a/SECURITY.md b/SECURITY.md index a9aa183e..2869ce58 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -20,10 +20,10 @@ We may backport fixes to the previous release at our discretion, but we don't gu We appreciate the efforts of security researchers and the community in identifying and reporting vulnerabilities. To ensure that potential vulnerabilities are addressed properly, please follow these guidelines: -1. Contact members of the team over E2EE private message. +1. **Contact members of the team directly** over E2EE private message. - [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link) - [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk) -2. **Email the security team** directly at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so don't include sensitive details. +2. **Email the security team** at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so don't include sensitive details. 3. **Do not disclose the vulnerability publicly** until it has been addressed 4. **Provide detailed information** about the vulnerability, including: - A clear description of the issue @@ -48,7 +48,7 @@ When you report a security vulnerability: When security vulnerabilities are identified: -1. We will develop and test fixes in a private branch +1. We will develop and test fixes in a private fork 2. Security updates will be released as soon as possible 3. Release notes will include information about the vulnerabilities, avoiding details that could facilitate exploitation where possible 4. Critical security updates may be backported to the previous stable release diff --git a/src/admin/debug/mod.rs b/src/admin/debug/mod.rs index 9b86f18c..1fd4e263 100644 --- a/src/admin/debug/mod.rs +++ b/src/admin/debug/mod.rs @@ -125,13 +125,13 @@ pub(super) enum DebugCommand { reset: bool, }, - /// - Verify json signatures + /// - Sign JSON blob /// /// This command needs a JSON blob provided in a Markdown code block below /// the command. SignJson, - /// - Verify json signatures + /// - Verify JSON signatures /// /// This command needs a JSON blob provided in a Markdown code block below /// the command. diff --git a/src/api/client/room/event.rs b/src/api/client/room/event.rs index 2b115b5c..61f8b88c 100644 --- a/src/api/client/room/event.rs +++ b/src/api/client/room/event.rs @@ -1,7 +1,7 @@ use axum::extract::State; -use conduwuit::{Err, Event, Result, err}; +use conduwuit::{Err, Event, PduEvent, Result, err}; use futures::{FutureExt, TryFutureExt, future::try_join}; -use ruma::api::client::room::get_room_event; +use ruma::api::client::{error::ErrorKind, room::get_room_event}; use crate::{Ruma, client::is_ignored_pdu}; @@ -14,6 +14,7 @@ pub(crate) async fn get_room_event_route( ) -> Result { let event_id = &body.event_id; let room_id = &body.room_id; + let sender_user = body.sender_user(); let event = services .rooms @@ -33,6 +34,52 @@ pub(crate) async fn get_room_event_route( return Err!(Request(Forbidden("You don't have permission to view this event."))); } + let include_unredacted_content = body + .include_unredacted_content // User's file has this field name + .unwrap_or(false); + + if include_unredacted_content && event.is_redacted() { + let is_server_admin = services + .users + .is_admin(sender_user) + .map(|is_admin| Ok(is_admin)); + let can_redact_privilege = services + .rooms + .state_accessor + .user_can_redact(event_id, sender_user, room_id, false) // federation=false for local check + ; + let (is_server_admin, can_redact_privilege) = + try_join(is_server_admin, can_redact_privilege).await?; + + if !is_server_admin && !can_redact_privilege { + return Err!(Request(Forbidden( + "You don't have permission to view redacted content.", + ))); + } + + let pdu_id = match services.rooms.timeline.get_pdu_id(event_id).await { + | Ok(id) => id, + | Err(e) => { + return Err(e); + }, + }; + let original_content = services + .rooms + .timeline + .get_original_pdu_content(&pdu_id) + .await?; + if let Some(original_content) = original_content { + // If the original content is available, we can return it. + // event.content = to_raw_value(&original_content)?; + event = PduEvent::from_id_val(event_id, original_content)?; + } else { + return Err(conduwuit::Error::BadRequest( + ErrorKind::UnredactedContentDeleted { content_keep_ms: None }, + "The original unredacted content is not in the database.", + )); + } + } + debug_assert!( event.event_id() == event_id && event.room_id() == room_id, "Fetched PDU must match requested" diff --git a/src/api/client/unversioned.rs b/src/api/client/unversioned.rs index 232d5b28..98976522 100644 --- a/src/api/client/unversioned.rs +++ b/src/api/client/unversioned.rs @@ -40,6 +40,7 @@ pub(crate) async fn get_supported_versions_route( "v1.11".to_owned(), ], unstable_features: BTreeMap::from_iter([ + ("fi.mau.msc2815".to_owned(), true), ("org.matrix.e2e_cross_signing".to_owned(), true), ("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */ ("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */ diff --git a/src/core/config/check.rs b/src/core/config/check.rs index ded9533d..3dc45e2f 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -219,6 +219,15 @@ pub fn check(config: &Config) -> Result { )); } + // Check if support contact information is configured + if config.well_known.support_email.is_none() && config.well_known.support_mxid.is_none() { + warn!( + "No support contact information (support_email or support_mxid) is configured in \ + the well_known section. Users in the admin room will be automatically listed as \ + support contacts in the /.well-known/matrix/support endpoint." + ); + } + if config .url_preview_domain_contains_allowlist .contains(&"*".to_owned()) diff --git a/src/database/maps.rs b/src/database/maps.rs index 19f9ced4..c72ed414 100644 --- a/src/database/maps.rs +++ b/src/database/maps.rs @@ -121,6 +121,15 @@ pub(super) static MAPS: &[Descriptor] = &[ index_size: 512, ..descriptor::SEQUENTIAL }, + Descriptor { + name: "pduid_originalcontent", + cache_disp: CacheDisp::SharedWith("pduid_pdu"), + key_size_hint: Some(16), + val_size_hint: Some(1520), + block_size: 2048, + index_size: 512, + ..descriptor::RANDOM + }, Descriptor { name: "publicroomids", ..descriptor::RANDOM_SMALL diff --git a/src/service/rooms/timeline/data.rs b/src/service/rooms/timeline/data.rs index 94c78bb0..e5baf3b3 100644 --- a/src/service/rooms/timeline/data.rs +++ b/src/service/rooms/timeline/data.rs @@ -19,6 +19,8 @@ pub(super) struct Data { pduid_pdu: Arc, userroomid_highlightcount: Arc, userroomid_notificationcount: Arc, + /// Stores the original content of redacted PDUs. + pduid_originalcontent: Arc, pub(super) db: Arc, services: Services, } @@ -38,6 +40,7 @@ impl Data { pduid_pdu: db["pduid_pdu"].clone(), userroomid_highlightcount: db["userroomid_highlightcount"].clone(), userroomid_notificationcount: db["userroomid_notificationcount"].clone(), + pduid_originalcontent: db["pduid_originalcontent"].clone(), // Initialize new table db: args.db.clone(), services: Services { short: args.depend::("rooms::short"), @@ -177,6 +180,24 @@ impl Data { self.pduid_pdu.get(pdu_id).await.deserialized() } + /// Stores the original content of a PDU that is about to be redacted. + pub(super) async fn store_redacted_pdu_content( + &self, + pdu_id: &RawPduId, + pdu_json: &CanonicalJsonObject, + ) -> Result<()> { + self.pduid_originalcontent.raw_put(pdu_id, Json(pdu_json)); + Ok(()) + } + + /// Returns the original content of a redacted PDU. + pub(super) async fn get_original_pdu_content( + &self, + pdu_id: &RawPduId, + ) -> Result> { + self.pduid_originalcontent.get(pdu_id).await.deserialized() + } + pub(super) async fn append_pdu( &self, pdu_id: &RawPduId, diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 4b2f3cb2..07bf9b7a 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -260,6 +260,25 @@ impl Service { self.db.replace_pdu(pdu_id, pdu_json, pdu).await } + /// Stores the content of a to-be redacted pdu. + #[tracing::instrument(skip(self), level = "debug")] + pub async fn store_redacted_pdu_content( + &self, + pdu_id: &RawPduId, + pdu_json: &CanonicalJsonObject, + ) -> Result<()> { + self.db.store_redacted_pdu_content(pdu_id, pdu_json).await + } + + /// Returns the original content of a redacted PDU. + #[tracing::instrument(skip(self), level = "debug")] + pub async fn get_original_pdu_content( + &self, + pdu_id: &RawPduId, + ) -> Result> { + self.db.get_original_pdu_content(pdu_id).await + } + /// Creates a new persisted data unit and adds it to a room. /// /// By this point the incoming event should be fully authenticated, no auth @@ -472,7 +491,7 @@ impl Service { .user_can_redact(redact_id, &pdu.sender, &pdu.room_id, false) .await? { - self.redact_pdu(redact_id, pdu, shortroomid).await?; + self.redact_pdu(redact_id, pdu, shortroomid, true).await?; } } }, @@ -485,7 +504,7 @@ impl Service { .user_can_redact(redact_id, &pdu.sender, &pdu.room_id, false) .await? { - self.redact_pdu(redact_id, pdu, shortroomid).await?; + self.redact_pdu(redact_id, pdu, shortroomid, true).await?; } } }, @@ -1033,6 +1052,7 @@ impl Service { event_id: &EventId, reason: &PduEvent, shortroomid: ShortRoomId, + keep_original_content: bool, ) -> Result { // TODO: Don't reserialize, keep original json let Ok(pdu_id) = self.get_pdu_id(event_id).await else { @@ -1054,6 +1074,19 @@ impl Service { let room_version_id = self.services.state.get_room_version(&pdu.room_id).await?; + if keep_original_content && !pdu.is_redacted() { + let original_pdu_json = utils::to_canonical_object(&pdu).map_err(|e| { + err!(Database(error!( + ?event_id, + ?e, + "Failed to convert PDU to canonical JSON for original content storage" + ))) + })?; + self.db + .store_redacted_pdu_content(&pdu_id, &original_pdu_json) + .await?; + } + pdu.redact(&room_version_id, reason)?; let obj = utils::to_canonical_object(&pdu).map_err(|e| {