From 5ca8be7323f0ada5043873b300e4582723ba9eda Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 27 Mar 2026 19:37:58 -0400 Subject: [PATCH] matrix: guard invalid HTML entity mention labels --- .../src/matrix/monitor/mentions.test.ts | 31 +++++++++++++++++++ .../matrix/src/matrix/monitor/mentions.ts | 20 +++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/extensions/matrix/src/matrix/monitor/mentions.test.ts b/extensions/matrix/src/matrix/monitor/mentions.test.ts index 0e264a46b51..ca1f872d874 100644 --- a/extensions/matrix/src/matrix/monitor/mentions.test.ts +++ b/extensions/matrix/src/matrix/monitor/mentions.test.ts @@ -209,6 +209,37 @@ describe("resolveMentions", () => { expect(result.wasMentioned).toBe(true); }); + it("ignores out-of-range hexadecimal HTML entities in visible labels", () => { + expect(() => + resolveMentions({ + content: { + msgtype: "m.text", + body: "hello", + formatted_body: ': hello', + }, + userId, + text: "hello", + mentionRegexes: [], + }), + ).not.toThrow(); + }); + + it("ignores oversized decimal HTML entities in visible labels", () => { + expect(() => + resolveMentions({ + content: { + msgtype: "m.text", + body: "hello", + formatted_body: + ': hello', + }, + userId, + text: "hello", + mentionRegexes: [], + }), + ).not.toThrow(); + }); + it("does not detect mention when displayName is spoofed", () => { const result = resolveMentions({ content: { diff --git a/extensions/matrix/src/matrix/monitor/mentions.ts b/extensions/matrix/src/matrix/monitor/mentions.ts index f1388de2652..4f1fc99a334 100644 --- a/extensions/matrix/src/matrix/monitor/mentions.ts +++ b/extensions/matrix/src/matrix/monitor/mentions.ts @@ -9,17 +9,29 @@ const HTML_ENTITY_REPLACEMENTS: Readonly> = { nbsp: " ", quot: '"', }; +const MAX_UNICODE_SCALAR_VALUE = 0x10ffff; + +function decodeNumericHtmlEntity(match: string, rawValue: string, radix: 10 | 16): string { + const codePoint = Number.parseInt(rawValue, radix); + if ( + !Number.isSafeInteger(codePoint) || + codePoint < 0 || + codePoint > MAX_UNICODE_SCALAR_VALUE || + (codePoint >= 0xd800 && codePoint <= 0xdfff) + ) { + return match; + } + return String.fromCodePoint(codePoint); +} function decodeHtmlEntities(value: string): string { return value.replace(/&(#x?[0-9a-f]+|\w+);/gi, (match, entity: string) => { const normalized = entity.toLowerCase(); if (normalized.startsWith("#x")) { - const codePoint = Number.parseInt(normalized.slice(2), 16); - return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint); + return decodeNumericHtmlEntity(match, normalized.slice(2), 16); } if (normalized.startsWith("#")) { - const codePoint = Number.parseInt(normalized.slice(1), 10); - return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint); + return decodeNumericHtmlEntity(match, normalized.slice(1), 10); } return HTML_ENTITY_REPLACEMENTS[normalized] ?? match; });