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; });