diff --git a/CHANGELOG.md b/CHANGELOG.md index 4effd50a59..6aae07ecda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16) +==================================================================================================== +Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr) + + Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10) ==================================================================================================== ## ✨ Features diff --git a/package.json b/package.json index cad743895b..f5b6464b60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element-web", - "version": "1.11.111", + "version": "1.11.112", "description": "Element: the future of secure communication", "author": "New Vector Ltd.", "repository": { diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index e3b01cae0b..041430340e 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -143,7 +143,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { // If the room is upgraded, use that room instead. We'll also splice out // any children of the room. - const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, false, msc3946ProcessDynamicPredecessor); + const history = this.matrixClient?.getRoomUpgradeHistory(room.roomId, true, msc3946ProcessDynamicPredecessor); if (history && history.length > 1) { room = history[history.length - 1]; // Last room is most recent in history diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts index 3fd3afae33..f5fb48df07 100644 --- a/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { logger } from "matrix-js-sdk/src/logger"; import { EventType } from "matrix-js-sdk/src/matrix"; -import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix"; +import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix"; import type { MatrixDispatcher } from "../../dispatcher/dispatcher"; import type { ActionPayload } from "../../dispatcher/payloads"; import type { FilterKey } from "./skip-list/filters"; @@ -248,12 +248,15 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { // If we're joining an upgraded room, we'll want to make sure we don't proliferate // the dead room in the list. if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) { - const roomState: RoomState = payload.room.currentState; - const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor); - if (predecessor) { - const prevRoom = this.matrixClient?.getRoom(predecessor.roomId); - if (prevRoom) this.roomSkipList.removeRoom(prevRoom); - else logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`); + const room: Room = payload.room; + const roomUpgradeHistory = room.client.getRoomUpgradeHistory( + room.roomId, + true, + this.msc3946ProcessDynamicPredecessor, + ); + const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room)); + for (const predecessor of predecessors) { + this.roomSkipList.removeRoom(predecessor); } } diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 2fc396b8b3..c6675d7aa5 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import { type MatrixClient, type Room, type RoomState, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix"; +import { type MatrixClient, type Room, EventType, type EmptyObject } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -308,24 +308,22 @@ export class RoomListStoreClass extends AsyncStoreWithClient implem const oldMembership = getEffectiveMembership(membershipPayload.oldMembership); const newMembership = getEffectiveMembershipTag(membershipPayload.room, membershipPayload.membership); if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) { - // If we're joining an upgraded room, we'll want to make sure we don't proliferate - // the dead room in the list. - const roomState: RoomState = membershipPayload.room.currentState; - const predecessor = roomState.findPredecessor(this.msc3946ProcessDynamicPredecessor); - if (predecessor) { - const prevRoom = this.matrixClient?.getRoom(predecessor.roomId); - if (prevRoom) { - const isSticky = this.algorithm.stickyRoom === prevRoom; - if (isSticky) { - this.algorithm.setStickyRoom(null); - } - - // Note: we hit the algorithm instead of our handleRoomUpdate() function to - // avoid redundant updates. - this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved); - } else { - logger.warn(`Unable to find predecessor room with id ${predecessor.roomId}`); + // If we're joining an upgraded room, we'll want to make sure we don't proliferate the dead room in the list. + const room: Room = membershipPayload.room; + const roomUpgradeHistory = room.client.getRoomUpgradeHistory( + room.roomId, + true, + this.msc3946ProcessDynamicPredecessor, + ); + const predecessors = roomUpgradeHistory.slice(0, roomUpgradeHistory.indexOf(room)); + for (const predecessor of predecessors) { + const isSticky = this.algorithm.stickyRoom === predecessor; + if (isSticky) { + this.algorithm.setStickyRoom(null); } + // Note: we hit the algorithm instead of our handleRoomUpdate() function to + // avoid redundant updates. + this.algorithm.handleRoomUpdate(predecessor, RoomUpdateCause.RoomRemoved); } await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom); diff --git a/src/utils/leave-behaviour.ts b/src/utils/leave-behaviour.ts index 2ad6e7d4fe..8b7ee02f36 100644 --- a/src/utils/leave-behaviour.ts +++ b/src/utils/leave-behaviour.ts @@ -40,7 +40,7 @@ export async function leaveRoomBehaviour( let leavingAllVersions = true; const history = matrixClient.getRoomUpgradeHistory( roomId, - false, + true, SettingsStore.getValue("feature_dynamic_room_predecessors"), ); if (history && history.length > 0) { diff --git a/test/unit-tests/stores/BreadcrumbsStore-test.ts b/test/unit-tests/stores/BreadcrumbsStore-test.ts index 66cd88f41e..71ae11831d 100644 --- a/test/unit-tests/stores/BreadcrumbsStore-test.ts +++ b/test/unit-tests/stores/BreadcrumbsStore-test.ts @@ -112,7 +112,7 @@ describe("BreadcrumbsStore", () => { await dispatchJoinRoom(room.roomId); // We pass the value of the dynamic predecessor setting through - expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false); + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false); }); }); @@ -134,7 +134,7 @@ describe("BreadcrumbsStore", () => { await dispatchJoinRoom(room.roomId); // We pass the value of the dynamic predecessor setting through - expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true); + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true); }); }); diff --git a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index a5bf757d0f..03249dc1fc 100644 --- a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -28,6 +28,7 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; import * as utils from "../../../../src/utils/notifications"; import * as roomMute from "../../../../src/stores/room-list/utils/roomMute"; import { Action } from "../../../../src/dispatcher/actions"; +import { mocked } from "jest-mock"; describe("RoomListStoreV3", () => { async function getRoomListStore() { @@ -197,6 +198,9 @@ describe("RoomListStoreV3", () => { const oldRoom = rooms[32]; // Create a new room with a predecessor event that points to oldRoom const newRoom = new Room("!foonew:matrix.org", client, client.getSafeUserId(), {}); + mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) => + roomId === newRoom.roomId ? [oldRoom, newRoom] : [], + ); const createWithPredecessor = new MatrixEvent({ type: EventType.RoomCreate, sender: "@foo:foo.org", @@ -227,6 +231,41 @@ describe("RoomListStoreV3", () => { expect(roomIds).toContain(newRoom.roomId); }); + it("should not remove predecessor room based on non-reciprocated relationship", async () => { + const { store, rooms, client, dispatcher } = await getRoomListStore(); + const oldRoom = rooms[32]; + // Create a new room with a predecessor event that points to oldRoom, but oldRoom does not point back + const newRoom = new Room("!nefarious:matrix.org", client, client.getSafeUserId(), {}); + const createWithPredecessor = new MatrixEvent({ + type: EventType.RoomCreate, + sender: "@foo:foo.org", + room_id: newRoom.roomId, + content: { + predecessor: { room_id: oldRoom.roomId, event_id: "tombstone_event_id" }, + }, + event_id: "$create", + state_key: "", + }); + upsertRoomStateEvents(newRoom, [createWithPredecessor]); + + const fn = jest.fn(); + store.on(LISTS_UPDATE_EVENT, fn); + dispatcher.dispatch( + { + action: "MatrixActions.Room.myMembership", + oldMembership: KnownMembership.Invite, + membership: KnownMembership.Join, + room: newRoom, + }, + true, + ); + + expect(fn).toHaveBeenCalled(); + const roomIds = store.getSortedRooms().map((r) => r.roomId); + expect(roomIds).toContain(oldRoom.roomId); + expect(roomIds).toContain(newRoom.roomId); + }); + it("Rooms are re-inserted on m.direct event", async () => { const { store, dispatcher, client } = await getRoomListStore(); diff --git a/test/unit-tests/stores/room-list/RoomListStore-test.ts b/test/unit-tests/stores/room-list/RoomListStore-test.ts index d1ab2e7183..f6e1700ae3 100644 --- a/test/unit-tests/stores/room-list/RoomListStore-test.ts +++ b/test/unit-tests/stores/room-list/RoomListStore-test.ts @@ -115,6 +115,10 @@ describe("RoomListStore", () => { // Given a store we can spy on const { store, handleRoomUpdate } = createStore(); + mocked(client.getRoomUpgradeHistory).mockImplementation((roomId) => + roomId === roomWithCreatePredecessor.roomId ? [oldRoom, roomWithCreatePredecessor] : [], + ); + // When we tell it we joined a new room that has an old room as // predecessor in the create event const payload = { diff --git a/test/unit-tests/utils/leave-behaviour-test.ts b/test/unit-tests/utils/leave-behaviour-test.ts index 23be7fef98..9da796a3a5 100644 --- a/test/unit-tests/utils/leave-behaviour-test.ts +++ b/test/unit-tests/utils/leave-behaviour-test.ts @@ -129,7 +129,7 @@ describe("leaveRoomBehaviour", () => { it("Passes through the dynamic predecessor setting", async () => { await leaveRoomBehaviour(client, room.roomId); - expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false); + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, false); }); }); @@ -143,7 +143,7 @@ describe("leaveRoomBehaviour", () => { it("Passes through the dynamic predecessor setting", async () => { await leaveRoomBehaviour(client, room.roomId); - expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true); + expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, true, true); }); }); });