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