Compare commits

...

7 commits

Author SHA1 Message Date
Jade Ellis
aa29b81ef6
fix: Don't store events that have already been redacted
Some checks failed
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, linux-amd64) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, linux-arm64) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Rust Checks / Format (push) Failing after 1s
Rust Checks / Clippy (push) Failing after 13s
Rust Checks / Cargo Test (push) Failing after 10s
This prevents clobbering
2025-06-14 19:40:43 +01:00
Jade Ellis
46b1eeb2c8
feat: Allow retrieving redacted message content (msc2815)
Still to do:
- Handling the difference between content that we have deleted and
content we never received
- Deleting the original content on command or expiry

Another question is if we have to store the full original content?
Can we get by with just storing the 'content' field?
2025-06-14 19:40:43 +01:00
Jade Ellis
88ecf61d49
feat: Store the original content of redacted PDUs 2025-06-14 19:40:42 +01:00
Jade Ellis
5d44653e3a
fix: Incorrect command descriptions
Some checks failed
Documentation / Build and Deploy Documentation (push) Failing after 2s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, linux-amd64) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, linux-arm64) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Rust Checks / Format (push) Failing after 1s
Rust Checks / Clippy (push) Failing after 32s
Rust Checks / Cargo Test (push) Failing after 10s
2025-06-14 16:51:24 +01:00
Jade Ellis
44e60d0ea6
docs: Tiny phrasing changes to the security policy
Some checks failed
Documentation / Build and Deploy Documentation (push) Failing after 3s
Rust Checks / Format (push) Failing after 15s
Rust Checks / Clippy (push) Failing after 12s
Rust Checks / Cargo Test (push) Failing after 8s
2025-06-14 16:34:58 +01:00
Jade Ellis
d7514178ab
ci: Fix extra bracket in commit shorthash
Some checks failed
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, linux-amd64) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, linux-arm64) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Documentation / Build and Deploy Documentation (push) Failing after 3s
Rust Checks / Format (push) Failing after 1s
Rust Checks / Clippy (push) Failing after 12s
Rust Checks / Cargo Test (push) Failing after 8s
2025-06-13 14:30:26 +01:00
Jade Ellis
1d45e0b68c
feat: Add warning when admin users will be exposed as support contacts
Some checks failed
Documentation / Build and Deploy Documentation (push) Failing after 8s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, linux-amd64) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, linux-arm64) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Rust Checks / Format (push) Failing after 20s
Rust Checks / Clippy (push) Failing after 21s
Rust Checks / Cargo Test (push) Failing after 10s
2025-06-13 13:39:50 +01:00
9 changed files with 130 additions and 10 deletions

View file

@ -180,7 +180,7 @@ jobs:
file: "docker/Dockerfile" file: "docker/Dockerfile"
build-args: | build-args: |
GIT_COMMIT_HASH=${{ github.sha }}) 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_URL=${{github.event.repository.html_url }}
GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }} GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}

View file

@ -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: 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) - [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk) <!-- ? --> - [@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 3. **Do not disclose the vulnerability publicly** until it has been addressed
4. **Provide detailed information** about the vulnerability, including: 4. **Provide detailed information** about the vulnerability, including:
- A clear description of the issue - A clear description of the issue
@ -48,7 +48,7 @@ When you report a security vulnerability:
When security vulnerabilities are identified: 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 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 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 4. Critical security updates may be backported to the previous stable release

View file

@ -125,13 +125,13 @@ pub(super) enum DebugCommand {
reset: bool, reset: bool,
}, },
/// - Verify json signatures /// - Sign JSON blob
/// ///
/// This command needs a JSON blob provided in a Markdown code block below /// This command needs a JSON blob provided in a Markdown code block below
/// the command. /// the command.
SignJson, SignJson,
/// - Verify json signatures /// - Verify JSON signatures
/// ///
/// This command needs a JSON blob provided in a Markdown code block below /// This command needs a JSON blob provided in a Markdown code block below
/// the command. /// the command.

View file

@ -1,7 +1,7 @@
use axum::extract::State; 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 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}; use crate::{Ruma, client::is_ignored_pdu};
@ -14,6 +14,7 @@ pub(crate) async fn get_room_event_route(
) -> Result<get_room_event::v3::Response> { ) -> Result<get_room_event::v3::Response> {
let event_id = &body.event_id; let event_id = &body.event_id;
let room_id = &body.room_id; let room_id = &body.room_id;
let sender_user = body.sender_user();
let event = services let event = services
.rooms .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."))); 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!( debug_assert!(
event.event_id() == event_id && event.room_id() == room_id, event.event_id() == event_id && event.room_id() == room_id,
"Fetched PDU must match requested" "Fetched PDU must match requested"

View file

@ -40,6 +40,7 @@ pub(crate) async fn get_supported_versions_route(
"v1.11".to_owned(), "v1.11".to_owned(),
], ],
unstable_features: BTreeMap::from_iter([ unstable_features: BTreeMap::from_iter([
("fi.mau.msc2815".to_owned(), true),
("org.matrix.e2e_cross_signing".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) */ ("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) */ ("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */

View file

@ -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 if config
.url_preview_domain_contains_allowlist .url_preview_domain_contains_allowlist
.contains(&"*".to_owned()) .contains(&"*".to_owned())

View file

@ -121,6 +121,15 @@ pub(super) static MAPS: &[Descriptor] = &[
index_size: 512, index_size: 512,
..descriptor::SEQUENTIAL ..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 { Descriptor {
name: "publicroomids", name: "publicroomids",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL

View file

@ -19,6 +19,8 @@ pub(super) struct Data {
pduid_pdu: Arc<Map>, pduid_pdu: Arc<Map>,
userroomid_highlightcount: Arc<Map>, userroomid_highlightcount: Arc<Map>,
userroomid_notificationcount: Arc<Map>, userroomid_notificationcount: Arc<Map>,
/// Stores the original content of redacted PDUs.
pduid_originalcontent: Arc<Map>,
pub(super) db: Arc<Database>, pub(super) db: Arc<Database>,
services: Services, services: Services,
} }
@ -38,6 +40,7 @@ impl Data {
pduid_pdu: db["pduid_pdu"].clone(), pduid_pdu: db["pduid_pdu"].clone(),
userroomid_highlightcount: db["userroomid_highlightcount"].clone(), userroomid_highlightcount: db["userroomid_highlightcount"].clone(),
userroomid_notificationcount: db["userroomid_notificationcount"].clone(), userroomid_notificationcount: db["userroomid_notificationcount"].clone(),
pduid_originalcontent: db["pduid_originalcontent"].clone(), // Initialize new table
db: args.db.clone(), db: args.db.clone(),
services: Services { services: Services {
short: args.depend::<rooms::short::Service>("rooms::short"), short: args.depend::<rooms::short::Service>("rooms::short"),
@ -177,6 +180,24 @@ impl Data {
self.pduid_pdu.get(pdu_id).await.deserialized() 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<Option<CanonicalJsonObject>> {
self.pduid_originalcontent.get(pdu_id).await.deserialized()
}
pub(super) async fn append_pdu( pub(super) async fn append_pdu(
&self, &self,
pdu_id: &RawPduId, pdu_id: &RawPduId,

View file

@ -260,6 +260,25 @@ impl Service {
self.db.replace_pdu(pdu_id, pdu_json, pdu).await 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<Option<CanonicalJsonObject>> {
self.db.get_original_pdu_content(pdu_id).await
}
/// Creates a new persisted data unit and adds it to a room. /// Creates a new persisted data unit and adds it to a room.
/// ///
/// By this point the incoming event should be fully authenticated, no auth /// 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) .user_can_redact(redact_id, &pdu.sender, &pdu.room_id, false)
.await? .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) .user_can_redact(redact_id, &pdu.sender, &pdu.room_id, false)
.await? .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, event_id: &EventId,
reason: &PduEvent, reason: &PduEvent,
shortroomid: ShortRoomId, shortroomid: ShortRoomId,
keep_original_content: bool,
) -> Result { ) -> Result {
// TODO: Don't reserialize, keep original json // TODO: Don't reserialize, keep original json
let Ok(pdu_id) = self.get_pdu_id(event_id).await else { 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?; 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)?; pdu.redact(&room_version_id, reason)?;
let obj = utils::to_canonical_object(&pdu).map_err(|e| { let obj = utils::to_canonical_object(&pdu).map_err(|e| {