From 08dee96fff2d4e8e7462b7ba8a86f8459e6fe44f Mon Sep 17 00:00:00 2001 From: Bart Oostveen Date: Mon, 28 Sep 2026 09:42:53 +0200 Subject: [PATCH] matrix-continuwuity_latest: init from matrix-continuwuity, matrix-continuwuity: mark as vulnerable Not going for an override here because this is cleaner, as to avoid more divergence from master. Not-cherry-picked-because: avoiding breaking changes on master for security backport --- .../ma/matrix-continuwuity/package.nix | 13 + .../0001-fix-backport-SEC10.patch | 250 ++++++++++++++++++ .../0002-fix-backport-SEC28.patch | 37 +++ .../0003-fix-backport-SEC26.patch | 132 +++++++++ .../ma/matrix-continuwuity_latest/package.nix | 104 ++++++++ 5 files changed, 536 insertions(+) create mode 100644 pkgs/by-name/ma/matrix-continuwuity_latest/0001-fix-backport-SEC10.patch create mode 100644 pkgs/by-name/ma/matrix-continuwuity_latest/0002-fix-backport-SEC28.patch create mode 100644 pkgs/by-name/ma/matrix-continuwuity_latest/0003-fix-backport-SEC26.patch create mode 100644 pkgs/by-name/ma/matrix-continuwuity_latest/package.nix diff --git a/pkgs/by-name/ma/matrix-continuwuity/package.nix b/pkgs/by-name/ma/matrix-continuwuity/package.nix index bb1bd56c34b8..f1b055b21d6a 100644 --- a/pkgs/by-name/ma/matrix-continuwuity/package.nix +++ b/pkgs/by-name/ma/matrix-continuwuity/package.nix @@ -100,5 +100,18 @@ rustPlatform.buildRustPackage (finalAttrs: { ]; # Not a typo, continuwuity is a drop-in replacement for conduwuit. mainProgram = "conduwuit"; + knownVulnerabilities = [ + '' + Continuwuity 0.5.10 can no longer be securely ran, as it contains a critical security-related bug that is currently embargoed at the time of writing. + Continuwuity internally tracks this as SEC8, see also: https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.9.1 + + The latest version of Continuwuity has been moved to `matrix-continuwuity_latest` as to not introduce breaking changes. + It is highly advisable to upgrade to this version as fast as possible. When deploying Continuwuity using the NixOS module, you may do so using: + + `services.matrix-continuwuity.package = pkgs.matrix-continuwuity_latest`. + + The most notable breaking change of the 26.x release is the removal of LDAP. + '' + ]; }; }) diff --git a/pkgs/by-name/ma/matrix-continuwuity_latest/0001-fix-backport-SEC10.patch b/pkgs/by-name/ma/matrix-continuwuity_latest/0001-fix-backport-SEC10.patch new file mode 100644 index 000000000000..042b4b12aaf2 --- /dev/null +++ b/pkgs/by-name/ma/matrix-continuwuity_latest/0001-fix-backport-SEC10.patch @@ -0,0 +1,250 @@ +From d25c12c343b53ea90025c11808700ad6267f4e59 Mon Sep 17 00:00:00 2001 +From: timedout +Date: Wed, 29 Jul 2026 16:38:05 +0100 +Subject: [PATCH] fix(backport): SEC10 + +Reviewed-By: Ginger +Co-Authored-By: Erwan Leboucher + +(cherry picked from commit 71016a0d7f79289f3bf8d7816c1fec3c85c43153) +--- + src/api/client/sync/v5.rs | 133 +++++++++++++++++++++++++++++++------- + 1 file changed, 111 insertions(+), 22 deletions(-) + +diff --git a/src/api/client/sync/v5.rs b/src/api/client/sync/v5.rs +index 183f3aaac..03a4c1972 100644 +--- a/src/api/client/sync/v5.rs ++++ b/src/api/client/sync/v5.rs +@@ -1,6 +1,6 @@ + use std::{ + cmp::{self, Ordering}, +- collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}, ++ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + ops::Deref, + time::Duration, + }; +@@ -28,6 +28,7 @@ + use ruma::{ + DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId, + api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount}, ++ assign, + directory::RoomTypeFilter, + events::{ + AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType, +@@ -139,6 +140,13 @@ pub(crate) async fn sync_events_v5_route( + let (all_joined_rooms, all_invited_rooms, all_knocked_rooms) = + join3(all_joined_rooms, all_invited_rooms, all_knocked_rooms).await; + ++ let allowed_rooms: BTreeSet = all_joined_rooms ++ .iter() ++ .chain(all_invited_rooms.iter()) ++ .chain(all_knocked_rooms.iter()) ++ .cloned() ++ .collect(); ++ + let all_joined_rooms = all_joined_rooms.iter().map(AsRef::as_ref); + let all_invited_rooms = all_invited_rooms.iter().map(AsRef::as_ref); + let all_knocked_rooms = all_knocked_rooms.iter().map(AsRef::as_ref); +@@ -192,13 +200,14 @@ pub(crate) async fn sync_events_v5_route( + ) + .await; + +- fetch_subscriptions(services, sync_info, &known_rooms, &mut todo_rooms).await; ++ fetch_subscriptions(services, sync_info, &known_rooms, &allowed_rooms, &mut todo_rooms).await; + + response.rooms = process_rooms( + services, + sender_user, + next_batch, + all_invited_rooms.clone(), ++ all_knocked_rooms.clone(), + &todo_rooms, + &mut response, + &body, +@@ -208,6 +217,7 @@ pub(crate) async fn sync_events_v5_route( + if response.rooms.iter().all(|(id, r)| { + r.timeline.is_empty() + && r.required_state.is_empty() ++ && r.invite_state.is_none() + && !response.extensions.receipts.rooms.contains_key(id) + }) && response + .extensions +@@ -238,10 +248,17 @@ async fn fetch_subscriptions( + services: &Services, + (sender_user, sender_device, globalsince, body): SyncInfo<'_>, + known_rooms: &KnownRooms, ++ allowed_rooms: &BTreeSet, + todo_rooms: &mut TodoRooms, + ) { + let mut known_subscription_rooms = BTreeSet::new(); + for (room_id, room) in &body.room_subscriptions { ++ // Silently ignore subscriptions to rooms the user is not a member of ++ // (joined or invited). ++ if !allowed_rooms.contains(room_id) { ++ continue; ++ } ++ + let not_exists = services.rooms.metadata.exists(room_id).eq(&false); + + let is_disabled = services.rooms.metadata.is_disabled(room_id); +@@ -399,11 +416,13 @@ async fn handle_lists<'a, Rooms, AllRooms>( + BTreeMap::default() + } + ++#[allow(clippy::too_many_arguments)] + async fn process_rooms<'a, Rooms>( + services: &Services, + sender_user: &UserId, + next_batch: u64, + all_invited_rooms: Rooms, ++ all_knocked_rooms: Rooms, + todo_rooms: &TodoRooms, + response: &mut sync_events::v5::Response, + body: &sync_events::v5::Request, +@@ -416,38 +435,99 @@ async fn process_rooms<'a, Rooms>( + let roomsincecount = PduCount::Normal(*roomsince); + + let mut timestamp: Option<_> = None; +- let mut invite_state = None; + let (timeline_pdus, limited); + let new_room_id: &RoomId = (*room_id).as_ref(); + if all_invited_rooms.clone().any(is_equal_to!(new_room_id)) { ++ let Ok(invite_count) = services ++ .rooms ++ .state_cache ++ .get_invite_count(room_id, sender_user) ++ .await ++ else { ++ continue; ++ }; ++ ++ if *roomsince >= invite_count { ++ continue; ++ } ++ + // TODO: figure out a timestamp we can use for remote invites +- invite_state = services ++ let invite_state = services + .rooms + .state_cache + .invite_state(sender_user, room_id) + .await + .ok(); + +- (timeline_pdus, limited) = (VecDeque::new(), true); +- } else { +- TimelinePdus { pdus: timeline_pdus, limited } = match load_timeline( +- services, +- sender_user, +- room_id, +- Some(roomsincecount), +- Some(PduCount::from(next_batch)), +- *timeline_limit, +- ) +- .await +- { +- | Ok(value) => value, +- | Err(err) => { +- warn!("Encountered missing timeline in {}, error {}", room_id, err); +- continue; +- }, ++ rooms.insert(room_id.clone(), sync_events::v5::response::Room { ++ initial: Some(roomsince == &0), ++ invite_state, ++ limited: true, ++ ..Default::default() ++ }); ++ continue; ++ } ++ ++ if all_knocked_rooms.clone().any(is_equal_to!(new_room_id)) { ++ let Ok(knock_count) = services ++ .rooms ++ .state_cache ++ .get_knock_count(room_id, sender_user) ++ .await ++ else { ++ continue; + }; ++ ++ if *roomsince >= knock_count { ++ continue; ++ } ++ ++ let Ok(knock_state) = services ++ .rooms ++ .state_cache ++ .knock_state(sender_user, room_id) ++ .await ++ else { ++ continue; ++ }; ++ ++ rooms.insert( ++ room_id.clone(), ++ assign!(sync_events::v5::response::Room::new(), { ++ initial: Some(roomsince == &0), ++ invite_state: Some(knock_state), ++ limited: true, ++ }), ++ ); ++ continue; ++ } ++ ++ if !services ++ .rooms ++ .state_cache ++ .is_joined(sender_user, room_id) ++ .await ++ { ++ continue; + } + ++ TimelinePdus { pdus: timeline_pdus, limited } = match load_timeline( ++ services, ++ sender_user, ++ room_id, ++ Some(roomsincecount), ++ Some(PduCount::from(next_batch)), ++ *timeline_limit, ++ ) ++ .await ++ { ++ | Ok(value) => value, ++ | Err(err) => { ++ warn!("Encountered missing timeline in {}, error {}", room_id, err); ++ continue; ++ }, ++ }; ++ + if body.extensions.account_data.enabled == Some(true) { + response.extensions.account_data.rooms.insert( + room_id.to_owned(), +@@ -627,7 +707,7 @@ async fn process_rooms<'a, Rooms>( + }, + initial: Some(roomsince == &0), + is_dm: None, +- invite_state, ++ invite_state: None, + unread_notifications: UnreadNotificationsCount { + highlight_count: Some( + services +@@ -753,6 +833,15 @@ async fn collect_typing_events( + + let mut typing_response = sync_events::v5::response::Typing::default(); + for (room_id, (_, _, roomsince)) in todo_rooms { ++ if !services ++ .rooms ++ .state_cache ++ .is_joined(sender_user, room_id) ++ .await ++ { ++ continue; ++ } ++ + if services.rooms.typing.last_typing_update(room_id).await? <= *roomsince { + continue; + } +-- +2.55.0 + diff --git a/pkgs/by-name/ma/matrix-continuwuity_latest/0002-fix-backport-SEC28.patch b/pkgs/by-name/ma/matrix-continuwuity_latest/0002-fix-backport-SEC28.patch new file mode 100644 index 000000000000..d07419844698 --- /dev/null +++ b/pkgs/by-name/ma/matrix-continuwuity_latest/0002-fix-backport-SEC28.patch @@ -0,0 +1,37 @@ +From dcb079931a929880518015794d709f93651154ca Mon Sep 17 00:00:00 2001 +From: Ginger +Date: Tue, 11 Aug 2026 15:59:26 -0400 +Subject: [PATCH 1/2] fix(backport): SEC28 + +(cherry picked from commit 49a8f6f53b6f86ed6abb8952843cb3a38c530ada) +--- + src/service/threepid/mod.rs | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/src/service/threepid/mod.rs b/src/service/threepid/mod.rs +index dcc0b58ab..bda80f82e 100644 +--- a/src/service/threepid/mod.rs ++++ b/src/service/threepid/mod.rs +@@ -112,6 +112,10 @@ pub async fn send_validation_email( + // If a validation session already exists for this client secret, we can either + // reuse it with a new token or return early because it's already valid. + | Some(session) => { ++ if session.email != recipient.email { ++ return Err!(Request(InvalidParam("Wrong email for session."))); ++ } ++ + match session.validation_state { + | ValidationState::Validated => { + // If the existing session is already valid, don't send an email. +@@ -119,7 +123,7 @@ pub async fn send_validation_email( + }, + | ValidationState::Pending(ref mut token) => { + // Check ratelimiting for the target address. +- if self.ratelimiter.check_key(&recipient.email).is_err() { ++ if self.ratelimiter.check_key(&session.email).is_err() { + return Err(Error::BadRequest( + ErrorKind::LimitExceeded { retry_after: None }, + "You're sending emails too fast, try again in a few minutes.", +-- +2.55.0 + diff --git a/pkgs/by-name/ma/matrix-continuwuity_latest/0003-fix-backport-SEC26.patch b/pkgs/by-name/ma/matrix-continuwuity_latest/0003-fix-backport-SEC26.patch new file mode 100644 index 000000000000..8736fdbeea72 --- /dev/null +++ b/pkgs/by-name/ma/matrix-continuwuity_latest/0003-fix-backport-SEC26.patch @@ -0,0 +1,132 @@ +From a00a615799bc55bc093a6298735732565aa2b566 Mon Sep 17 00:00:00 2001 +From: Ginger +Date: Tue, 11 Aug 2026 16:35:24 -0400 +Subject: [PATCH 2/2] fix(backport): SEC26 + +Reviewed-By: timedout +Reviewed-By: Ginger +(cherry picked from commit 700fbe472d4a8385b96f870256bfc00f9a15f809) +--- + src/api/server/event_auth.rs | 27 ++++++++++----------------- + src/api/server/state.rs | 13 ++++++++++++- + src/api/server/state_ids.rs | 13 ++++++++++++- + 3 files changed, 34 insertions(+), 19 deletions(-) + +diff --git a/src/api/server/event_auth.rs b/src/api/server/event_auth.rs +index a9019e8e7..433e18f46 100644 +--- a/src/api/server/event_auth.rs ++++ b/src/api/server/event_auth.rs +@@ -1,12 +1,9 @@ + use std::{borrow::Borrow, iter::once}; + + use axum::extract::State; +-use conduwuit::{Err, Error, Result, info, utils::stream::ReadyExt}; ++use conduwuit::{Err, Result, Event, info, utils::stream::ReadyExt}; + use futures::StreamExt; +-use ruma::{ +- RoomId, +- api::{client::error::ErrorKind, federation::authorization::get_event_authorization}, +-}; ++use ruma::api::federation::authorization::get_event_authorization; + + use super::AccessCheck; + use crate::Ruma; +@@ -42,25 +39,21 @@ pub(crate) async fn get_event_authorization_route( + return Err!(Request(NotFound("This server is not participating in that room."))); + } + +- let event = services ++ // The event must be in the room we just authorised access to ++ if !services + .rooms + .timeline +- .get_pdu_json(&body.event_id) ++ .get_pdu(&body.event_id) + .await +- .map_err(|_| Error::BadRequest(ErrorKind::NotFound, "Event not found."))?; +- +- let room_id_str = event +- .get("room_id") +- .and_then(|val| val.as_str()) +- .ok_or_else(|| Error::bad_database("Invalid event in database."))?; +- +- let room_id = <&RoomId>::try_from(room_id_str) +- .map_err(|_| Error::bad_database("Invalid room_id in event in database."))?; ++ .is_ok_and(|pdu| pdu.room_id_or_hash() == body.room_id) ++ { ++ return Err!(Request(NotFound("Event not found."))); ++ } + + let auth_chain = services + .rooms + .auth_chain +- .event_ids_iter(room_id, once(body.event_id.borrow())) ++ .event_ids_iter(&body.room_id, once(body.event_id.borrow())) + .ready_filter_map(Result::ok) + .filter_map(|id| async move { services.rooms.timeline.get_pdu_json(&id).await.ok() }) + .then(|pdu| services.sending.convert_to_outgoing_federation_event(pdu)) +diff --git a/src/api/server/state.rs b/src/api/server/state.rs +index 5e1ad8ca2..05a008b74 100644 +--- a/src/api/server/state.rs ++++ b/src/api/server/state.rs +@@ -1,7 +1,7 @@ + use std::{borrow::Borrow, iter::once}; + + use axum::extract::State; +-use conduwuit::{Err, Result, at, err, info, utils::IterStream}; ++use conduwuit::{Err, Event, Result, at, err, info, utils::IterStream}; + use futures::{FutureExt, StreamExt, TryStreamExt}; + use ruma::{OwnedEventId, api::federation::event::get_room_state}; + +@@ -24,6 +24,17 @@ pub(crate) async fn get_room_state_route( + .check() + .await?; + ++ // The event must be in the room we just authorised access to ++ if !services ++ .rooms ++ .timeline ++ .get_pdu(&body.event_id) ++ .await ++ .is_ok_and(|pdu| pdu.room_id_or_hash() == body.room_id) ++ { ++ return Err!(Request(NotFound("Event not found."))); ++ } ++ + if !services + .rooms + .state_cache +diff --git a/src/api/server/state_ids.rs b/src/api/server/state_ids.rs +index c9dea3116..206f3efc5 100644 +--- a/src/api/server/state_ids.rs ++++ b/src/api/server/state_ids.rs +@@ -1,7 +1,7 @@ + use std::{borrow::Borrow, iter::once}; + + use axum::extract::State; +-use conduwuit::{Err, Result, at, err, info}; ++use conduwuit::{Err, Event, Result, at, err, info}; + use futures::{StreamExt, TryStreamExt}; + use ruma::{OwnedEventId, api::federation::event::get_room_state_ids}; + +@@ -25,6 +25,17 @@ pub(crate) async fn get_room_state_ids_route( + .check() + .await?; + ++ // The event must be in the room we just authorised access to ++ if !services ++ .rooms ++ .timeline ++ .get_pdu(&body.event_id) ++ .await ++ .is_ok_and(|pdu| pdu.room_id_or_hash() == body.room_id) ++ { ++ return Err!(Request(NotFound("Event not found."))); ++ } ++ + if !services + .rooms + .state_cache +-- +2.55.0 + diff --git a/pkgs/by-name/ma/matrix-continuwuity_latest/package.nix b/pkgs/by-name/ma/matrix-continuwuity_latest/package.nix new file mode 100644 index 000000000000..c94e635f7543 --- /dev/null +++ b/pkgs/by-name/ma/matrix-continuwuity_latest/package.nix @@ -0,0 +1,104 @@ +{ + lib, + rustPlatform, + fetchFromGitea, + pkg-config, + bzip2, + zstd, + stdenv, + rocksdb, + nix-update-script, + testers, + matrix-continuwuity_latest, + rust-jemalloc-sys-unprefixed, + liburing, + nixosTests, +}: +let + rocksdb' = + (rocksdb.override { + # rocksdb does not support prefixed jemalloc, which is required on darwin + enableJemalloc = !stdenv.hostPlatform.isDarwin; + jemalloc = rust-jemalloc-sys-unprefixed; + }).overrideAttrs + ( + final: old: { + version = "10.10.1"; + src = fetchFromGitea { + domain = "forgejo.ellis.link"; + owner = "continuwuation"; + repo = "rocksdb"; + rev = "10.10.fb"; + hash = "sha256-1ef75IDMs5Hba4VWEyXPJb02JyShy5k4gJfzGDhopRk="; + }; + + patches = [ ]; + } + ); +in +rustPlatform.buildRustPackage (finalAttrs: { + pname = "matrix-continuwuity"; + version = "0.5.10"; + + src = fetchFromGitea { + domain = "forgejo.ellis.link"; + owner = "continuwuation"; + repo = "continuwuity"; + tag = "v${finalAttrs.version}"; + hash = "sha256-oevEGYlAK/rMJhm200CkwerT5oVak8sJj0Fa6r6+J/Q="; + }; + + cargoHash = "sha256-uvMiFURXxkLbbbwq4pG5hevsLZHQ1wVfTNvzQRTQWxE="; + + patches = [ + ./0001-fix-backport-SEC10.patch + ./0002-fix-backport-SEC28.patch + ./0003-fix-backport-SEC26.patch + ]; + + nativeBuildInputs = [ + pkg-config + rustPlatform.bindgenHook + ]; + + buildInputs = [ + bzip2 + zstd + rust-jemalloc-sys-unprefixed + liburing + ]; + + env = { + ZSTD_SYS_USE_PKG_CONFIG = true; + ROCKSDB_INCLUDE_DIR = "${rocksdb'}/include"; + ROCKSDB_LIB_DIR = "${rocksdb'}/lib"; + }; + + passthru = { + rocksdb = rocksdb'; # make used rocksdb version available (e.g., for backup scripts) + updateScript = nix-update-script { }; + tests = { + version = testers.testVersion { + inherit (finalAttrs) version; + package = matrix-continuwuity_latest; + }; + } + // lib.optionalAttrs stdenv.hostPlatform.isLinux { + inherit (nixosTests) matrix-continuwuity; + }; + }; + + meta = { + description = "Matrix homeserver written in Rust, forked from conduwuit"; + homepage = "https://continuwuity.org/"; + changelog = "https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v${finalAttrs.version}"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ + bartoostveen + nyabinary + snaki + ]; + # Not a typo, continuwuity is a drop-in replacement for conduwuit. + mainProgram = "conduwuit"; + }; +})