Low severityOSV Advisory· Published Sep 16, 2025· Updated Apr 15, 2026
CVE-2025-59160
CVE-2025-59160
Description
Matrix JavaScript SDK is a Matrix Client-Server SDK for JavaScript and TypeScript. matrix-js-sdk before 38.2.0 has insufficient validation of room predecessor links in MatrixClient::getJoinedRooms, allowing a remote attacker to attempt to replace a tombstoned room with an unrelated attacker-supplied room. The issue has been patched and users should upgrade to 38.2.0. A workaround is to avoid using MatrixClient::getJoinedRooms in favor of getRooms() and filtering upgraded rooms separately.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
matrix-js-sdknpm | < 38.2.0 | 38.2.0 |
Affected products
1- Range: no-media-devices-release, v0.1.0, v0.1.1, …
Patches
22 files changed · +7 −1
CHANGELOG.md+6 −0 modified@@ -1,3 +1,9 @@ +Changes in [38.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0) (2025-09-16) +================================================================================================== +Fix [CVE-2025-59160](https://www.cve.org/CVERecord?id=CVE-2025-59160) / [GHSA-mp7c-m3rh-r56v](https://github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56v) + + + Changes in [38.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v38.1.0) (2025-09-09) ================================================================================================== ## ✨ Features
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "38.1.0", + "version": "38.2.0", "description": "Matrix Client-Server SDK for Javascript", "engines": { "node": ">=22.0.0"
43c72d5bf5e2Merge commit from fork
2 files changed · +30 −14
spec/unit/matrix-client.spec.ts+23 −0 modified@@ -2998,6 +2998,8 @@ describe("MatrixClient", function () { replacedByDynamicPredecessor2, room2, ]; + client.store.getRoom = (roomId: string) => + client.store.getRooms().find((r) => r.roomId === roomId) || null; room1.addLiveEvents( [ roomCreateEvent(room1.roomId, replacedByCreate1.roomId), @@ -3036,6 +3038,7 @@ describe("MatrixClient", function () { replacedByDynamicPredecessor2, }; } + it("Returns an empty list if there are no rooms", () => { client.store = new StubStore(); client.store.getRooms = () => []; @@ -3062,6 +3065,8 @@ describe("MatrixClient", function () { const room2 = new Room("room2", client, "@daryl:alexandria.example.com"); client.store = new StubStore(); client.store.getRooms = () => [room1, replacedRoom1, replacedRoom2, room2]; + client.store.getRoom = (roomId: string) => + client.store.getRooms().find((r) => r.roomId === roomId) || null; room1.addLiveEvents([roomCreateEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true }); room2.addLiveEvents([roomCreateEvent(room2.roomId, replacedRoom2.roomId)], { addToState: true }); replacedRoom1.addLiveEvents([tombstoneEvent(room1.roomId, replacedRoom1.roomId)], { addToState: true }); @@ -3127,6 +3132,24 @@ describe("MatrixClient", function () { expect(rooms).toContain(room1); expect(rooms).toContain(room2); }); + + it("should ignore room replacements which are not reciprocated by the predecessor", () => { + const room1 = new Room("room1", client, "@carol:alexandria.example.com"); + // Room 2 claims to replace room 1 but room 1 does not agree + const room2 = new Room("replacedRoom1", client, "@daryl:alexandria.example.com"); + + client.store = new StubStore(); + client.store.getRooms = () => [room1, room2]; + client.store.getRoom = (roomId: string) => + client.store.getRooms().find((r) => r.roomId === roomId) || null; + + room2.addLiveEvents([roomCreateEvent(room2.roomId, room1.roomId)], { addToState: true }); + + // When we ask for the visible rooms + const rooms = client.getVisibleRooms(); + expect(rooms).toContain(room1); + expect(rooms).toContain(room2); + }); }); describe("getRoomUpgradeHistory", () => {
src/client.ts+7 −14 modified@@ -2141,21 +2141,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa public getVisibleRooms(msc3946ProcessDynamicPredecessor = false): Room[] { const allRooms = this.store.getRooms(); - const replacedRooms = new Set(); - for (const r of allRooms) { - const predecessor = r.findPredecessor(msc3946ProcessDynamicPredecessor)?.roomId; - if (predecessor) { - replacedRooms.add(predecessor); + const visibleRooms = new Set(allRooms); + for (const room of visibleRooms) { + const predecessors = this.findPredecessorRooms(room, true, msc3946ProcessDynamicPredecessor); + for (const predecessor of predecessors) { + visibleRooms.delete(predecessor); } } - - return allRooms.filter((r) => { - const tombstone = r.currentState.getStateEvents(EventType.RoomTombstone, ""); - if (tombstone && replacedRooms.has(r.roomId)) { - return false; - } - return true; - }); + return Array.from(visibleRooms); } /** @@ -3852,7 +3845,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa roomId: string, includeFuture = true, ): Promise<{ [roomId: string]: Error | MatrixError | null }> { - const upgradeHistory = this.getRoomUpgradeHistory(roomId); + const upgradeHistory = this.getRoomUpgradeHistory(roomId, true); let eligibleToLeave = upgradeHistory; if (!includeFuture) {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-mp7c-m3rh-r56vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59160ghsaADVISORY
- github.com/matrix-org/matrix-js-sdk/commit/43c72d5bf5e2d0a26b3b4f71092e7cb39d4137c4nvdWEB
- github.com/matrix-org/matrix-js-sdk/releases/tag/v38.2.0ghsaWEB
- github.com/matrix-org/matrix-js-sdk/security/advisories/GHSA-mp7c-m3rh-r56vnvdWEB
- www.npmjs.com/package/matrix-js-sdk/v/38.2.0ghsaWEB
News mentions
0No linked articles in our index yet.