diff --git a/extensions/msteams/src/graph-thread.test.ts b/extensions/msteams/src/graph-thread.test.ts index b34b76ccc95..77164b1c469 100644 --- a/extensions/msteams/src/graph-thread.test.ts +++ b/extensions/msteams/src/graph-thread.test.ts @@ -37,6 +37,15 @@ describe("stripHtmlFromTeamsMessage", () => { ); }); + it("does not double-decode escaped entities (decodes & last)", () => { + // Graph encodes literally-typed entity text by escaping its '&' to '&'. + // Decoding '&' first would re-decode the now-bare '<'/'>' into + // angle brackets, corrupting the user's literal text. + expect(stripHtmlFromTeamsMessage("The token is &lt;APIKEY&gt;")).toBe( + "The token is <APIKEY>", + ); + }); + it("normalizes multiple whitespace to single space", () => { expect(stripHtmlFromTeamsMessage("hello world")).toBe("hello world"); }); diff --git a/extensions/msteams/src/graph-thread.ts b/extensions/msteams/src/graph-thread.ts index 88b37311991..4254af7698f 100644 --- a/extensions/msteams/src/graph-thread.ts +++ b/extensions/msteams/src/graph-thread.ts @@ -35,14 +35,16 @@ export function stripHtmlFromTeamsMessage(html: string): string { let text = html.replace(/]*>(.*?)<\/at>/gi, "@$1"); // Strip remaining HTML tags. text = text.replace(/<[^>]*>/g, " "); - // Decode common HTML entities. + // Decode common HTML entities. & must be decoded LAST to prevent + // double-decoding (e.g. &lt; → < not <), matching decodeHtmlEntities + // in inbound.ts. text = text - .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, '"') .replace(/'/g, "'") - .replace(/ /g, " "); + .replace(/ /g, " ") + .replace(/&/g, "&"); // Normalize whitespace. return text.replace(/\s+/g, " ").trim(); }