fix(invoice): Befunde aus dem Sicherheitsreview
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 9m45s

- E-Rechnung: in XML verbotene Steuerzeichen aus Namen/Anschriften entfernen,
  sonst waere das eingebettete XML bei einem einzigen Zeichen ungueltig.
- Rechnungskopie: genau eine Adresse; eine Liste ([email protected],[email protected]) wird beim
  Speichern abgelehnt und nie an nodemailer gegeben.
- Hinweis bei fehlgeschlagener Stornorechnung korrigiert (ein erneutes
  Stornieren ist nicht moeglich, die Stripe-Erstattung holt sie nach).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-12 06:57:29 +02:00
1 parent 2995890b07
commit 4827c8fa61
6 files changed
+21 -3

No files matched your search

+2 -1
View File
@@ -67,7 +67,8 @@ export async function updateSiteContentAction(formData: FormData): Promise<void>
}
// Die Rechnungskopie geht an genau diese Adresse: leer (keine Kopie) oder
// eine echte Adresse, sonst verschwaenden die Kopien stillschweigend.
if (def.key === INVOICE_COPY_KEY && valueDe.trim() !== "" && normalizeContactEmail(valueDe) === null) {
// Genau eine Adresse: "[email protected], [email protected]" waere fuer nodemailer eine Liste.
if (def.key === INVOICE_COPY_KEY && valueDe.trim() !== "" && !/^[^,;]+$/.test(normalizeContactEmail(valueDe) ?? "")) {
return fail(
settingsPathForGroup(group),
"„Rechnungskopie an“ ist keine gültige E-Mail-Adresse. Bitte z. B. [email protected] eintragen oder das Feld leer lassen."
+1 -1
View File
@@ -328,7 +328,7 @@ export async function updateOrderStatusAction(formData: FormData): Promise<void>
await issueCancellationInvoice(orderId, "STORNIERUNG");
} catch (error: unknown) {
console.error(`[order-cancel] Stornorechnung fuer ${result.orderNumber} fehlgeschlagen:`, error);
stornoWarnings.push("Die Stornorechnung konnte nicht erstellt werden — bitte die Stornierung erneut auslösen oder den Support informieren.");
stornoWarnings.push("Die Stornorechnung konnte nicht erstellt werden. Sie entsteht spätestens mit der Stripe-Erstattung; sonst bitte den Support informieren.");
}
if (stornoWarnings.length > 0) {
+4
View File
@@ -168,4 +168,8 @@ describe("Hilfsfunktionen", () => {
test("escapeXml maskiert alle fuenf Sonderzeichen", () => {
expect(escapeXml(`<a href="x">'&'</a>`)).toBe("&lt;a href=&quot;x&quot;&gt;&apos;&amp;&apos;&lt;/a&gt;");
});
test("escapeXml entfernt in XML verbotene Steuerzeichen, behaelt Tab und Zeilenumbruch", () => {
expect(escapeXml("Er\u0001ika\u000B\u001F Muster\tfrau\n")).toBe("Erika Muster\tfrau\n");
});
});
+6
View File
@@ -80,8 +80,14 @@ export interface EInvoiceInput {
export type EInvoiceResult = { ok: true; xml: string } | { ok: false; reason: string };
/**
* Maskiert Text fuer das XML. Steuerzeichen, die XML 1.0 gar nicht erlaubt,
* fallen weg: Namen und Anschriften tippt die Kundschaft selbst, und ein
* einziges solches Zeichen machte das eingebettete XML ungueltig.
*/
export function escapeXml(value: string): string {
return value
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F￾￿]/g, "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
+5
View File
@@ -48,6 +48,11 @@ describe("Rechnungskopie", () => {
);
});
test("eine Adressliste wird nicht verwendet", async () => {
values[INVOICE_COPY_KEY] = "[email protected],[email protected]";
expect(await getInvoiceCopyAddress()).toBeNull();
});
test("maskiert Kundendaten im HTML-Teil", () => {
const { html, text } = invoiceCopyContent(copy);
expect(html).toContain("Erika &lt;Musterfrau&gt;");
+3 -1
View File
@@ -18,7 +18,9 @@ export async function getInvoiceCopyAddress(): Promise<string | null> {
try {
const values = await getContentValues([INVOICE_COPY_KEY], "de");
const raw = values[INVOICE_COPY_KEY]?.trim() ?? "";
return raw ? normalizeContactEmail(raw) : null;
// Genau eine Adresse (siehe Pruefung beim Speichern); eine Liste nie an nodemailer weiterreichen.
const address = raw ? normalizeContactEmail(raw) : null;
return address && !/[,;]/.test(address) ? address : null;
} catch (error: unknown) {
console.error("[invoice-mail] Adresse fuer die Rechnungskopie nicht lesbar:", error);
return null;