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]>
This commit is contained in:
1 parent
dcfad4e011
commit
cef2248a45
6 files changed
+126
-35
No files matched your search
+3
-11
@@ -1,4 +1,5 @@
|
||||
import type { Locale } from "@/i18n/config";
|
||||
import { escapeHtml } from "@/lib/escape-html";
|
||||
|
||||
/**
|
||||
* Elektronische Widerrufsfunktion (§ 356a BGB): Wortlaut der Erklaerung und
|
||||
@@ -18,7 +19,7 @@ export interface WithdrawalDeclarationInput {
|
||||
export interface WithdrawalReceiptEmail {
|
||||
subject: string;
|
||||
preheader: string;
|
||||
/** Inhalt fuer den gemeinsamen Mail-Rahmen (emailLayout), bereits maskiert. */
|
||||
/** Inhalt fuer den gemeinsamen Mail-Rahmen (emailLayout), bereits maskiert. preheader maskiert emailLayout selbst. */
|
||||
bodyHtml: string;
|
||||
text: string;
|
||||
}
|
||||
@@ -83,15 +84,6 @@ export function withdrawalDeclarationLines(input: WithdrawalDeclarationInput, lo
|
||||
];
|
||||
}
|
||||
|
||||
export function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/** Zeilenumbrueche im Betreff wuerden sonst als weitere Mail-Header gelesen (Header-Injection). */
|
||||
function singleLine(value: string): string {
|
||||
return value.replace(/[\r\n]+/g, " ");
|
||||
@@ -117,7 +109,7 @@ export function withdrawalReceiptEmailContent(
|
||||
const text = [s.heading, "", s.intro, "", ...lines, "", `${s.receivedAt}: ${when}`, "", s.next].join("\n");
|
||||
return {
|
||||
subject: singleLine(s.subject(input.orderNumber)),
|
||||
preheader: escapeHtml(singleLine(`${s.receivedAt}: ${when}`)),
|
||||
preheader: singleLine(`${s.receivedAt}: ${when}`),
|
||||
bodyHtml,
|
||||
text,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user