matrix: guard invalid HTML entity mention labels

This commit is contained in:
Gustavo Madeira Santana 2026-03-27 19:37:58 -04:00
parent eef2f82986
commit 5ca8be7323
No known key found for this signature in database
2 changed files with 47 additions and 4 deletions

View File

@ -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: '<a href="https://matrix.to/#/@bot:matrix.org">&#x110000;</a>: 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:
'<a href="https://matrix.to/#/@bot:matrix.org">&#9999999999999999999999999999999999999999;</a>: hello',
},
userId,
text: "hello",
mentionRegexes: [],
}),
).not.toThrow();
});
it("does not detect mention when displayName is spoofed", () => {
const result = resolveMentions({
content: {

View File

@ -9,17 +9,29 @@ const HTML_ENTITY_REPLACEMENTS: Readonly<Record<string, string>> = {
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;
});