fix(security): Rechnungsablage kann nicht mehr ins oeffentliche Upload-Verzeichnis geraten
app/uploads/[...path] liefert JEDE Datei unterhalb von UPLOAD_DIR ohne Anmeldung aus — das ist fuer Produktfotos gewollt. Dass Rechnungen nicht darunter liegen, war bisher aber allein eine Frage der richtigen Konfiguration: ein INVOICE_DIR="/app/uploads/rechnungen" haette genuegt, damit fremde Rechnungen samt Anschrift und Bestellinhalt fuer jeden abrufbar sind, der den Pfad erraet. Das waere im Betrieb nicht aufgefallen, denn funktioniert haette weiterhin alles: Ablegen, Versenden und Abrufen ueber /rechnung/<nummer>/pdf haetten sich genauso verhalten wie vorher. Nur haette daneben eine zweite, ungeschuetzte Tuer zu denselben Dateien gestanden. Drei Sicherungen statt einer Anmerkung in der Dokumentation: - lib/invoice-storage.ts wirft beim Laden des Moduls, wenn INVOICE_DIR innerhalb von UPLOAD_DIR liegt. Lieber startet die Anwendung nicht, als dass sie Belege oeffentlich ausliefert. - "pnpm preflight" meldet denselben Fall als Blocker, also bereits vor dem Deploy statt erst beim Start. - Ein Test haelt die Trennung fest, damit sie nicht beim naechsten Umbau unbemerkt verloren geht. Der uebrige Sicherheitsdurchgang ueber die Aenderungen dieser Sitzung ergab keine ausnutzbaren Befunde: der Admin-Bereich ist dreifach abgesichert (Proxy, Layout, jede Server Action einzeln), jeweils autoritativ gegen die Datenbank und mit erzwungener Zwei-Faktor-Authentifizierung; das returnTo des Sync-Knopfes wird gegen eine feste Liste geprueft; Pfade aus der Datenbank werden weiterhin gegen ihr Wurzelverzeichnis validiert. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
92ac826baa
commit
6135c1f9c0
3 files changed
+59
No files matched your search
@@ -20,6 +20,7 @@
|
||||
* als ungeprueft gemeldet statt geraten.
|
||||
*/
|
||||
import "dotenv/config";
|
||||
import path from "node:path";
|
||||
import { prisma } from "../src/lib/prisma";
|
||||
import { getContentValue } from "../src/lib/content";
|
||||
import { locales } from "../src/i18n/config";
|
||||
@@ -130,6 +131,20 @@ should(
|
||||
`Eingetragen ist "${vatRate}". Ueblich sind 0 (§ 19 UStG), 7 (ermaessigt) oder 19 (voll) — ein anderer Wert ist moeglich, aber pruefenswert.`
|
||||
);
|
||||
|
||||
// --- Rechnungsablage -------------------------------------------------------
|
||||
// Das Upload-Verzeichnis wird oeffentlich und ohne Anmeldung ausgeliefert
|
||||
// (app/uploads/[...path]). Liegt die Rechnungsablage darin, sind fremde
|
||||
// Rechnungen samt Anschrift und Bestellinhalt fuer jeden abrufbar, der den
|
||||
// Pfad erraet — und im Betrieb faellt das nicht auf, weil weiterhin alles
|
||||
// funktioniert.
|
||||
const invoiceDir = path.resolve(value("INVOICE_DIR") || "./invoices");
|
||||
const uploadDir = path.resolve(value("UPLOAD_DIR") || "./uploads");
|
||||
must(
|
||||
"INVOICE_DIR liegt ausserhalb von UPLOAD_DIR",
|
||||
invoiceDir !== uploadDir && !invoiceDir.startsWith(uploadDir + path.sep),
|
||||
`Rechnungen (${invoiceDir}) duerfen nicht unterhalb des oeffentlich ausgelieferten Upload-Verzeichnisses (${uploadDir}) liegen. Eigenes Verzeichnis waehlen, siehe .env.example.`
|
||||
);
|
||||
|
||||
// --- Datenbank -------------------------------------------------------------
|
||||
must("DATABASE_URL gesetzt", isSet("DATABASE_URL"), "Ohne Datenbank startet die Anwendung nicht.");
|
||||
|
||||
|
||||
Reference in new issue
Block a user