CVE-2023-38700: Insufficient Information (NVD-CWE-noinfo)
value: matrix-appservice-irc is a Node.js IRC bridge for Matrix. Prior to version 1.0.1, it was possible to craft an event such that it would leak part of a targeted message event from another bridged room. This required knowing an event ID to target. Version 1.0.1n fixes this issue. As a workaround, set the `matrixHandler.eventCacheSize` config value to `0`. This workaround may impact performance.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 3.7
Published: 2023-08-04T19:15Z
Repository: https://github.com/matrix-org/matrix-appservice-irc
Language: TypeScript
Files changed: src/bridge/MatrixHandler.ts
Commit date: 2023-07-31 09:40:04 +0100
Commit message: Merge pull request from GHSA-c7hh-3v6c-fj4q


Diff: diff --git a/src/bridge/MatrixHandler.ts b/src/bridge/MatrixHandler.ts
index c460f873b..1864ca7d0 100644
--- a/src/bridge/MatrixHandler.ts
+++ b/src/bridge/MatrixHandler.ts
@@ -117,20 +117,21 @@ interface CachedEvent {
     sender: string;
     timestamp: number;
 }
 
 export class MatrixHandler {
-    private readonly processingInvitesForRooms: {
-        [roomIdUserId: string]: Promise<unknown>;
-    } = {};
     // maintain a list of room IDs which are being processed invite-wise. This is
     // required because invites are processed asyncly, so you could get invite->msg
     // and the message is processed before the room is created.
+    private readonly processingInvitesForRooms: {
+        [roomIdUserId: string]: Promise<unknown>;
+    } = {};
+    // Map of `roomId-eventId` -> cached event
     private readonly eventCache: Map<string, CachedEvent> = new Map();
     private readonly metrics: {[domain: string]: {
-        [metricName: string]: number;
-    };} = {};
+            [metricName: string]: number;
+        };} = {};
     private readonly mediaUrl: string;
     private memberTracker?: StateLookup;
     private adminHandler: AdminRoomHandler;
     private config: MatrixHandlerConfig = DEFAULTS;
 
@@ -1046,11 +1047,11 @@ export class MatrixHandler {
         }
 
         // special handling for edits
         if (event.content["m.relates_to"]?.rel_type === "m.replace") {
             const originalEventId = event.content["m.relates_to"].event_id;
-            let originalBody = this.getCachedEvent(originalEventId)?.body;
+            let originalBody = this.getCachedEvent(event.room_id, originalEventId)?.body;
             if (!originalBody) {
                 try {
                     // FIXME: this will return the new event rather than the original one
                     // to actually see the original content we'd need to use whatever
                     // https://github.com/matrix-org/matrix-doc/pull/2675 stabilizes on
@@ -1085,15 +1086,19 @@ export class MatrixHandler {
         const nextNewLine = body.indexOf("\n");
         if (nextNewLine !== -1) {
             body = body.substring(0, nextNewLine);
         }
         // Cache events in here so we can refer to them for replies.
-        this.cacheEvent(event.event_id, {
-            body: cacheBody,
-            sender: event.sender,
-            timestamp: event.origin_server_ts,
-        });
+        this.cacheEvent(
+            event.room_id,
+            event.event_id,
+            {
+                body: cacheBody,
+                sender: event.sender,
+                timestamp: event.origin_server_ts,
+            },
+        );
 
         // The client might still be connected, for abundance of safety let's wait.
         await ircClient.waitForConnected();
 
         // Generate an array of individual messages that would be sent
@@ -1266,24 +1271,24 @@ export class MatrixHandler {
             }
             return match[3];
         };
 
         const REPLY_NAME_MAX_LENGTH = 12;
-        const eventId = replyEventId;
         if (!event.content.body) {
             return null;
         }
 
         const rplText = replyText(event.content.body);
         let rplName: string;
         let rplSource: string;
-        let cachedEvent = this.getCachedEvent(eventId);
+        // Reply must be in the same room as the original event.
+        let cachedEvent = this.getCachedEvent(event.room_id, replyEventId);
         if (!cachedEvent) {
             // Fallback to fetching from the homeserver.
             try {
                 const eventContent = await this.ircBridge.getAppServiceBridge().getIntent().getEvent(
-                    event.room_id, eventId
+                    event.room_id, replyEventId
                 );
                 rplName = eventContent.sender;
                 if (typeof(eventContent.content.body) !== "string") {
                     throw Error("'body' was not a string.");
                 }
@@ -1294,11 +1299,11 @@ export class MatrixHandler {
                 }
                 else {
                     rplSource = eventContent.content.body;
                 }
                 cachedEvent = {sender: rplName, body: rplSource, timestamp: eventContent.origin_server_ts};
-                this.cacheEvent(eventId, cachedEvent);
+                this.cacheEvent(eventContent.room_id, eventContent.event_id, cachedEvent);
             }
             catch (err) {
                 // If we couldn't find the event, then frankly we can't
                 // trust it and we won't treat it as a reply.
                 return {
@@ -1373,21 +1378,23 @@ export class MatrixHandler {
             metricSet[metricName]++;
         }
         this.metrics[serverDomain] = metricSet;
     }
 
-    private cacheEvent(id: string, event: CachedEvent) {
-        this.eventCache.set(id, event);
+    private cacheEvent(roomId: string, eventId: string, event: CachedEvent) {
+        const cacheKey = `${roomId}-${eventId}`;
+        this.eventCache.set(cacheKey, event);
 
         if (this.eventCache.size > this.config.eventCacheSize) {
             const delKey = this.eventCache.entries().next().value[0];
             this.eventCache.delete(delKey);
         }
     }
 
-    private getCachedEvent(id: string): CachedEvent|undefined {
-        return this.eventCache.get(id);
+    private getCachedEvent(roomId: string, eventId: string): CachedEvent|undefined {
+        const cacheKey = `${roomId}-${eventId}`;
+        return this.eventCache.get(cacheKey);
     }
 
     // EXPORTS
     public onConfigChanged(config: MatrixHandlerConfig|undefined) {
         this.config = {...DEFAULTS, ...config};

Vulnerable code:     private readonly processingInvitesForRooms: {
        [roomIdUserId: string]: Promise<unknown>;
    } = {};
        [metricName: string]: number;
    };} = {};
            let originalBody = this.getCachedEvent(originalEventId)?.body;
        this.cacheEvent(event.event_id, {
            body: cacheBody,
            sender: event.sender,
            timestamp: event.origin_server_ts,
        });
        const eventId = replyEventId;
        let cachedEvent = this.getCachedEvent(eventId);
                    event.room_id, eventId
                this.cacheEvent(eventId, cachedEvent);
    private cacheEvent(id: string, event: CachedEvent) {
        this.eventCache.set(id, event);
    private getCachedEvent(id: string): CachedEvent|undefined {
        return this.eventCache.get(id);
Fixed code:     private readonly processingInvitesForRooms: {
        [roomIdUserId: string]: Promise<unknown>;
    } = {};
    // Map of `roomId-eventId` -> cached event
            [metricName: string]: number;
        };} = {};
            let originalBody = this.getCachedEvent(event.room_id, originalEventId)?.body;
        this.cacheEvent(
            event.room_id,
            event.event_id,
            {
                body: cacheBody,
                sender: event.sender,
                timestamp: event.origin_server_ts,
            },
        );
        // Reply must be in the same room as the original event.
        let cachedEvent = this.getCachedEvent(event.room_id, replyEventId);
                    event.room_id, replyEventId
                this.cacheEvent(eventContent.room_id, eventContent.event_id, cachedEvent);
    private cacheEvent(roomId: string, eventId: string, event: CachedEvent) {
        const cacheKey = `${roomId}-${eventId}`;
        this.eventCache.set(cacheKey, event);
    private getCachedEvent(roomId: string, eventId: string): CachedEvent|undefined {
        const cacheKey = `${roomId}-${eventId}`;
        return this.eventCache.get(cacheKey);