Pentest 2026-05-20 LOW/INFO Sammelfix

27.1 Path-Traversal-Strings in DB:
- cleanupConsents validierte documentPath zuvor nur per stripHtml,
  ließ "../../../etc/passwd" durch. Neuer isValidDocumentPath-Check
  akzeptiert nur "/uploads/<safe>", alles andere → NULL.
- cleanupDocumentPaths scannt fünf weitere Tabellen (BankCard,
  IdentityDocument, Invoice, RepresentativeAuthorization nullable;
  ContractDocument NOT NULL → nur Report).

Orphaned User:
- reportOrphanedUsers warnt beim Container-Start vor User ohne
  Rollenzuordnung (im Permission-System unsichtbar). Löschen nicht
  automatisch wegen False-Positive-Risiko.

Seed-PW-Policy:
- generateInitialPassword() nutzte Math.random() (vorhersagbar).
  Jetzt crypto.randomInt() für Pick + Fisher-Yates-Shuffle.

PUT /users/:id mit permissions / password:
- Vorher silent-drop durch Whitelist + HTTP 200, Caller glaubte
  faelschlich, Werte waeren uebernommen. Jetzt HTTP 400 mit
  konkreter Hilfe-Message.

/api/health ohne Auth:
- Pentest-Befund INFO: bewusst so, Container-Healthcheck und
  Reverse-Proxy pingen ohne Bearer-Token. Antwort liefert nur
  {status,timestamp} – keine Version, kein DB-Status, kein
  Info-Leak. Comment im Code dokumentiert die Entscheidung.

Live-verifiziert auf dev: alle fuenf Findings durchgetestet,
jeweils mit dirty Input → erwartete Sanitization/Antwort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 07:49:06 +02:00
parent adc3b70492
commit 8e48d3b432
5 changed files with 184 additions and 13 deletions
@@ -92,11 +92,26 @@ const ALLOWED_CONSENT_SOURCES = new Set([
'crm-backend',
]);
// Legitimer documentPath: relativer Pfad unter uploads/, keine ".."-Segmente.
// Schreibend werden Pfade ausschließlich vom multer-Upload erzeugt
// (server-kontrollierter Dateiname), bestehende Pentest-Hinterlassenschaften
// wie "../../../etc/passwd" oder "javascript:alert(1)" müssen aus der DB
// raus (Pentest 2026-05-20 LOW 27.1).
function isValidDocumentPath(v: string | null | undefined): boolean {
if (!v) return true; // null/leer ist OK
if (v.includes('..')) return false;
if (/(?:javascript|data|vbscript)\s*:/i.test(v)) return false;
if (/<[a-z!\/]/i.test(v)) return false; // HTML im Pfad
// erlaubt: "uploads/...", "/uploads/..."; keine Kontrollzeichen
return /^\/?uploads\/[A-Za-z0-9._\-\/]+$/.test(v);
}
async function cleanupConsents() {
// version + documentPath: HTML strippen (waren ohne Validierung).
// source: Whitelist erzwingen.
// documentPath zusätzlich gegen Pfad-Traversal absichern (27.1).
let versionStripped = 0;
let pathStripped = 0;
let pathNulled = 0;
let sourceFixed = 0;
const consents = await prisma.customerConsent.findMany({
select: { id: true, source: true, documentPath: true, version: true },
@@ -107,9 +122,11 @@ async function cleanupConsents() {
data.version = stripHtmlString(c.version);
versionStripped++;
}
if (c.documentPath && c.documentPath !== stripHtmlString(c.documentPath)) {
data.documentPath = stripHtmlString(c.documentPath);
pathStripped++;
if (c.documentPath && !isValidDocumentPath(c.documentPath)) {
// ".../etc/passwd", "<script>", "javascript:..." etc. → NULL.
// Legitime Uploads bleiben unberührt (siehe isValidDocumentPath).
data.documentPath = null;
pathNulled++;
}
if (c.source && !ALLOWED_CONSENT_SOURCES.has(c.source)) {
data.source = 'unknown';
@@ -121,10 +138,100 @@ async function cleanupConsents() {
}
console.log(
` → Consent bereinigt: version-stripped=${versionStripped}, ` +
`documentPath-stripped=${pathStripped}, source-whitelist=${sourceFixed}`,
`documentPath-genullt=${pathNulled}, source-whitelist=${sourceFixed}`,
);
}
// documentPath in den weiteren Tabellen prüfen. Schreibend wird er
// server-seitig vom multer-Upload erzeugt falls dort doch mal ein
// dreckiger Wert reingerutscht ist (z.B. aus einem importierten Backup
// vor unseren Sanitization-Fixes), nullen wir ihn hier raus.
// ContractDocument hat documentPath NOT NULL → wir berichten dort nur,
// löschen aber nicht (Records müssten manuell angeschaut werden).
async function cleanupDocumentPaths() {
const findings: { table: string; id: number; value: string }[] = [];
const optional: Array<{
label: string;
fetch: () => Promise<{ id: number; documentPath: string | null }[]>;
update: (id: number) => Promise<unknown>;
}> = [
{
label: 'BankCard',
fetch: () => prisma.bankCard.findMany({ select: { id: true, documentPath: true } }),
update: (id) => prisma.bankCard.update({ where: { id }, data: { documentPath: null } }),
},
{
label: 'IdentityDocument',
fetch: () => prisma.identityDocument.findMany({ select: { id: true, documentPath: true } }),
update: (id) => prisma.identityDocument.update({ where: { id }, data: { documentPath: null } }),
},
{
label: 'Invoice',
fetch: () => prisma.invoice.findMany({ select: { id: true, documentPath: true } }),
update: (id) => prisma.invoice.update({ where: { id }, data: { documentPath: null } }),
},
{
label: 'RepresentativeAuthorization',
fetch: () => prisma.representativeAuthorization.findMany({
select: { id: true, documentPath: true },
}),
update: (id) => prisma.representativeAuthorization.update({
where: { id }, data: { documentPath: null },
}),
},
];
let nulled = 0;
for (const t of optional) {
const rows = await t.fetch();
for (const r of rows) {
if (r.documentPath && !isValidDocumentPath(r.documentPath)) {
findings.push({ table: t.label, id: r.id, value: r.documentPath.slice(0, 80) });
await t.update(r.id);
nulled++;
}
}
}
// ContractDocument: documentPath ist NOT NULL → wir berichten nur.
const contractDocs = await prisma.contractDocument.findMany({
select: { id: true, documentPath: true },
});
let contractDocsDirty = 0;
for (const d of contractDocs) {
if (!isValidDocumentPath(d.documentPath)) {
findings.push({ table: 'ContractDocument', id: d.id, value: d.documentPath.slice(0, 80) });
contractDocsDirty++;
}
}
console.log(` → documentPath bereinigt: ${nulled} genullt, ${contractDocsDirty} ContractDocument-Records auffällig (NOT NULL, manuell prüfen)`);
for (const f of findings.slice(0, 10)) {
console.log(` [${f.table}#${f.id}] "${f.value}"`);
}
}
async function reportOrphanedUsers() {
// User ohne jegliche Rollenzuordnung können sich zwar einloggen, sind aber
// im Permission-System unsichtbar. Meist Überrest von gescheiterten Seeds
// oder manuellen DB-Edits. Wir löschen NICHT (könnte legitime
// Spezial-User treffen) nur warnen.
const orphans = await prisma.user.findMany({
where: { roles: { none: {} } },
select: { id: true, email: true, createdAt: true },
});
if (orphans.length === 0) {
console.log(' → Keine User ohne Rollenzuordnung.');
return;
}
console.log(` ⚠️ ${orphans.length} User ohne Rollenzuordnung:`);
for (const u of orphans.slice(0, 10)) {
console.log(` [User#${u.id}] ${u.email} (created ${u.createdAt.toISOString()})`);
}
console.log(' → Rolle zuweisen oder User löschen.');
}
async function cleanupAppSettings() {
const settings = await prisma.appSetting.findMany();
const removed: string[] = [];
@@ -228,6 +335,8 @@ async function main() {
await cleanupXss();
await cleanupAppSettings();
await cleanupConsents();
await cleanupDocumentPaths();
await reportOrphanedUsers();
await findOrPurgePentestRecords();
console.log('=== Fertig. ===');
}