fix(invoice): Befunde aus dem Sicherheitsreview
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 9m45s
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:
1 parent
2995890b07
commit
4827c8fa61
6 files changed
+21
-3
No files matched your search
@@ -67,7 +67,8 @@ export async function updateSiteContentAction(formData: FormData): Promise<void>
|
|||||||
}
|
}
|
||||||
// Die Rechnungskopie geht an genau diese Adresse: leer (keine Kopie) oder
|
// Die Rechnungskopie geht an genau diese Adresse: leer (keine Kopie) oder
|
||||||
// eine echte Adresse, sonst verschwaenden die Kopien stillschweigend.
|
// 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(
|
return fail(
|
||||||
settingsPathForGroup(group),
|
settingsPathForGroup(group),
|
||||||
"„Rechnungskopie an“ ist keine gültige E-Mail-Adresse. Bitte z. B. [email protected] eintragen oder das Feld leer lassen."
|
"„Rechnungskopie an“ ist keine gültige E-Mail-Adresse. Bitte z. B. [email protected] eintragen oder das Feld leer lassen."
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ export async function updateOrderStatusAction(formData: FormData): Promise<void>
|
|||||||
await issueCancellationInvoice(orderId, "STORNIERUNG");
|
await issueCancellationInvoice(orderId, "STORNIERUNG");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(`[order-cancel] Stornorechnung fuer ${result.orderNumber} fehlgeschlagen:`, error);
|
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) {
|
if (stornoWarnings.length > 0) {
|
||||||
|
|||||||
@@ -168,4 +168,8 @@ describe("Hilfsfunktionen", () => {
|
|||||||
test("escapeXml maskiert alle fuenf Sonderzeichen", () => {
|
test("escapeXml maskiert alle fuenf Sonderzeichen", () => {
|
||||||
expect(escapeXml(`<a href="x">'&'</a>`)).toBe("<a href="x">'&'</a>");
|
expect(escapeXml(`<a href="x">'&'</a>`)).toBe("<a href="x">'&'</a>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -80,8 +80,14 @@ export interface EInvoiceInput {
|
|||||||
|
|
||||||
export type EInvoiceResult = { ok: true; xml: string } | { ok: false; reason: string };
|
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 {
|
export function escapeXml(value: string): string {
|
||||||
return value
|
return value
|
||||||
|
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "")
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
|
|||||||
@@ -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", () => {
|
test("maskiert Kundendaten im HTML-Teil", () => {
|
||||||
const { html, text } = invoiceCopyContent(copy);
|
const { html, text } = invoiceCopyContent(copy);
|
||||||
expect(html).toContain("Erika <Musterfrau>");
|
expect(html).toContain("Erika <Musterfrau>");
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export async function getInvoiceCopyAddress(): Promise<string | null> {
|
|||||||
try {
|
try {
|
||||||
const values = await getContentValues([INVOICE_COPY_KEY], "de");
|
const values = await getContentValues([INVOICE_COPY_KEY], "de");
|
||||||
const raw = values[INVOICE_COPY_KEY]?.trim() ?? "";
|
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) {
|
} catch (error: unknown) {
|
||||||
console.error("[invoice-mail] Adresse fuer die Rechnungskopie nicht lesbar:", error);
|
console.error("[invoice-mail] Adresse fuer die Rechnungskopie nicht lesbar:", error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in new issue
Block a user