Skip to content

Commit cbaa4a4

Browse files
KrisBraunclaude
andcommitted
fix(google): HTML-escape the emoji in the reaction email's html fallback
The reaction email's text/html fallback interpolated the emoji directly into markup. A real emoji never contains HTML metacharacters, but the value is ultimately a user-supplied reaction string (only custom-emoji refs are pre-filtered, not arbitrary content), so escape it to keep the outbound email's HTML body free of markup injection. The text/plain and JSON reaction parts were already safe (base64 / JSON-encoded). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0222695 commit cbaa4a4

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

‎connectors/google/src/mail/gmail-api.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,23 @@ describe("buildReactionMessage", () => {
664664
// still show something.
665665
expect(decodeMimePart(raw, "text/plain")).toBe("💖");
666666
});
667+
668+
it("HTML-escapes the emoji in the html fallback (no markup injection)", () => {
669+
const raw = decodeRawMessage(
670+
buildReactionMessage({
671+
to: ["alice@example.com"],
672+
cc: [],
673+
from: "me@example.com",
674+
subject: "Hi",
675+
emoji: "<img src=x onerror=alert(1)>",
676+
messageId: "<orig@mail.gmail.com>",
677+
references: "",
678+
})
679+
);
680+
const html = decodeMimePart(raw, "text/html");
681+
expect(html).not.toContain("<img src=x");
682+
expect(html).toContain("&lt;img src=x onerror=alert(1)&gt;");
683+
});
667684
});
668685

669686
describe("isSendableGmailReaction", () => {

‎connectors/google/src/mail/gmail-api.ts‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1883,6 +1883,16 @@ export function isSendableGmailReaction(emoji: string): boolean {
18831883
return true;
18841884
}
18851885

1886+
/** Escapes the five HTML metacharacters so a value is safe inside HTML text. */
1887+
function escapeHtml(value: string): string {
1888+
return value
1889+
.replace(/&/g, "&amp;")
1890+
.replace(/</g, "&lt;")
1891+
.replace(/>/g, "&gt;")
1892+
.replace(/"/g, "&quot;")
1893+
.replace(/'/g, "&#39;");
1894+
}
1895+
18861896
/**
18871897
* Builds a Gmail emoji-reaction email for the message identified by
18881898
* `messageId` (its RFC Message-ID). Per
@@ -1928,7 +1938,12 @@ export function buildReactionMessage(options: {
19281938
const altBoundary = mimeBoundary("alt");
19291939
const enc = new TextEncoder();
19301940
const reactionJson = JSON.stringify({ version: 1, emoji });
1931-
const htmlFallback = `<!DOCTYPE html>\r\n<html><body>\r\n<p>${emoji}</p>\r\n</body></html>`;
1941+
// HTML-escape the emoji before interpolating it into the html fallback part.
1942+
// A real emoji never contains HTML metacharacters, but `emoji` is ultimately a
1943+
// user-supplied reaction string (only custom-emoji refs are pre-filtered, not
1944+
// arbitrary content), so escape defensively to keep the outbound email's HTML
1945+
// body injection-free.
1946+
const htmlFallback = `<!DOCTYPE html>\r\n<html><body>\r\n<p>${escapeHtml(emoji)}</p>\r\n</body></html>`;
19321947

19331948
const altBlock = [
19341949
`Content-Type: multipart/alternative; boundary="${altBoundary}"`,

0 commit comments

Comments
 (0)