Files
Wabenhain/src/lib/withdrawal.test.ts
T
Nick SzaunigandClaude Opus 5.5 cef2248a45 fix(security): escape untrusted values in transactional email HTML
Customer input (first name, address, product names), order data, admin
fields (company data, carrier, tracking number) and URLs were inserted
into email HTML unescaped. A first name such as <a href="…">…</a> ended
up as a working link in a genuine shop email, usable for phishing.

All variable values in email HTML now go through escapeHtml
(lib/escape-html.ts, shared with the withdrawal receipt). Button links
only accept http(s), so a javascript: tracking URL is dropped. The plain
text part stays unescaped. Tests cover order confirmation, shipment,
status mails, layout company data and back-in-stock mails.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 17:42:49 +02:00

74 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { formatReceivedAt, withdrawalDeclarationLines, withdrawalReceiptEmailContent } from "./withdrawal";
import { escapeHtml } from "./escape-html";
const input = {
name: "Erika Mustermann",
orderNumber: "WH-2026-0042",
email: "[email protected]",
message: null,
};
// 26.09.2026 12:03:07 UTC = 14:03:07 in Berlin (Sommerzeit)
const receivedAt = new Date(Date.UTC(2026, 8, 26, 12, 3, 7));
describe("formatReceivedAt", () => {
it("formats in German local time with seconds", () => {
expect(formatReceivedAt(receivedAt, "de")).toBe("26.09.2026, 14:03:07 Uhr (MESZ)");
});
it("uses winter time in winter", () => {
expect(formatReceivedAt(new Date(Date.UTC(2026, 0, 5, 9, 0, 0)), "de")).toBe("05.01.2026, 10:00:00 Uhr (MEZ)");
});
it("formats in English with the German time zone", () => {
expect(formatReceivedAt(receivedAt, "en")).toBe("26 September 2026, 14:03:07 (CEST, German time)");
});
});
describe("withdrawalDeclarationLines", () => {
it("states the withdrawal and the identifying data", () => {
expect(withdrawalDeclarationLines(input, "de")).toEqual([
"Hiermit widerrufe ich den Vertrag über die Bestellung WH-2026-0042.",
"Name: Erika Mustermann",
"E-Mail: [email protected]",
]);
});
it("adds an optional message", () => {
expect(withdrawalDeclarationLines({ ...input, message: "Falsche Sorte" }, "en")).toEqual([
"I hereby withdraw from the contract for order WH-2026-0042.",
"Name: Erika Mustermann",
"Email: [email protected]",
"Message: Falsche Sorte",
]);
});
});
describe("escapeHtml", () => {
it("neutralises markup", () => {
expect(escapeHtml(`<a href="x">'&'</a>`)).toBe("&lt;a href=&quot;x&quot;&gt;&#39;&amp;&#39;&lt;/a&gt;");
});
});
describe("withdrawalReceiptEmailContent", () => {
it("contains the declaration and the time of receipt in text and HTML", () => {
const mail = withdrawalReceiptEmailContent(input, receivedAt, "de");
expect(mail.subject).toBe("Eingangsbestätigung deines Widerrufs zu WH-2026-0042");
expect(mail.text).toContain("Hiermit widerrufe ich den Vertrag über die Bestellung WH-2026-0042.");
expect(mail.text).toContain("26.09.2026, 14:03:07 Uhr (MESZ)");
expect(mail.bodyHtml).toContain("26.09.2026, 14:03:07 Uhr (MESZ)");
});
it("escapes user input in the HTML body and strips line breaks from the subject", () => {
const mail = withdrawalReceiptEmailContent(
{ ...input, name: `<img src=x onerror=alert(1)>`, orderNumber: "A1\r\nBcc: [email protected]" },
receivedAt,
"de"
);
expect(mail.bodyHtml).not.toContain("<img");
expect(mail.bodyHtml).toContain("&lt;img src=x onerror=alert(1)&gt;");
expect(mail.subject).not.toMatch(/[\r\n]/);
});
});