Files
opencrm/backend/scripts/seed-magic-byte-test.ts
T
duffyduckandClaude Opus 4.8 f3668b8a5a Magic-Byte-Seed: aus Prod-Image ausschliessen + Prod-Riegel
Der Pentester-Helfer soll nicht ins Produktions-Image und darf nie gegen
Prod laufen:
- .dockerignore schliesst backend/scripts/seed-magic-byte-test.ts aus dem
  Build-Context aus -> Datei liegt nicht mehr im Prod-Container (bleibt im
  Repo fuer Dev/Staging-Checkout).
- Script bricht bei NODE_ENV=production im "create"-Modus hart ab (exit 1);
  "cleanup" bleibt erlaubt, damit man immer aufraeumen kann.

Verifiziert: create@production -> Abbruch exit 1; cleanup@production laeuft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 13:49:32 +02:00

159 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Pentester-Hilfsscript (Pentest R152): legt einen KONTROLLIERTEN „getarnten"
* Upload an, um den Magic-Byte-Mismatch→attachment-Zweig des Download-Endpoints
* live zu belegen.
*
* Was passiert:
* - Es wird eine dedizierte TEST-Bankkarte (accountHolder-Marker) angelegt
* ECHTE Kundendaten werden NICHT angefasst/überschrieben.
* - An diese Karte wird eine Datei mit `.pdf`-Endung gehängt, deren Inhalt
* aber ein SVG mit <script> ist (Non-Whitelist-Magic-Byte, klassischer
* Stored-XSS-Payload).
* - documentPath wird exakt gesetzt, damit findUploadOwner die Datei der
* Test-Bankkarte (→ Customer) zuordnet und der Ownership-Check greift.
*
* Erwartetes Verhalten am Endpoint
* GET /api/files/download?path=<pfad>&disposition=inline :
* → HTTP 200, aber `Content-Disposition: attachment` (NICHT inline),
* `X-Content-Type-Options: nosniff` → Browser lädt herunter, rendert
* NICHT im iframe → kein Stored-XSS.
* → Backend-Log: "[fileDownload] inline angefragt, aber Magic-Byte-Check
* fehlgeschlagen: <pfad>"
* Ohne `disposition=inline` ohnehin attachment (Default).
* Als fremder Portal-User: 403 (Ownership-Check, Karte gehört Test-Customer).
*
* Aufruf (im Backend-App-Verzeichnis / Container-WORKDIR /app):
* npx tsx scripts/seed-magic-byte-test.ts create [--customer <id>]
* npx tsx scripts/seed-magic-byte-test.ts cleanup
*
* Danach unbedingt `cleanup` laufen lassen (löscht Datei + Test-Bankkarte).
*/
import fs from 'fs';
import path from 'path';
import prisma from '../src/lib/prisma.js';
const MARKER = 'MAGICBYTE-TEST (Pentester R152)';
const FILENAME = 'magic-byte-test.pdf'; // .pdf-Endung, aber SVG-Inhalt (getarnt)
const UPLOAD_SUBDIR = 'bank-cards';
const RELATIVE_PATH = `/uploads/${UPLOAD_SUBDIR}/${FILENAME}`;
// SVG mit <script> würde bei inline-Auslieferung als image/svg+xml im Browser
// ausgeführt. Genau das muss der Magic-Byte-Gate verhindern.
const SVG_XSS = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="80">
<text x="10" y="40">MAGICBYTE-TEST</text>
<script type="text/javascript">/* Pentest R152 */ alert('XSS-MAGICBYTE-TEST');</script>
</svg>
`;
function uploadsDir(): string {
return path.join(process.cwd(), 'uploads', UPLOAD_SUBDIR);
}
function absoluteFilePath(): string {
return path.join(uploadsDir(), FILENAME);
}
async function create(customerIdArg?: number): Promise<void> {
// Harter Prod-Riegel: dieses Script legt eine Fake-Bankkarte + getarnte
// Datei an das darf NIE gegen Produktion laufen (weder versehentlich noch
// absichtlich). `cleanup` bleibt erlaubt, damit man immer aufräumen kann.
if (process.env.NODE_ENV === 'production') {
console.error(
'ABBRUCH: "create" ist bei NODE_ENV=production deaktiviert. Dieser ' +
'Pentester-/Test-Helfer gehört nur nach Dev/Staging. (Zum Aufräumen ist ' +
'"cleanup" weiterhin erlaubt.)',
);
process.exit(1);
}
// Ziel-Customer bestimmen (Default: erster vorhandener Kunde).
const customer = customerIdArg
? await prisma.customer.findUnique({ where: { id: customerIdArg }, select: { id: true, customerNumber: true } })
: await prisma.customer.findFirst({ orderBy: { id: 'asc' }, select: { id: true, customerNumber: true } });
if (!customer) {
console.error('Kein Kunde gefunden bitte zuerst einen Kunden anlegen oder --customer <id> angeben.');
process.exit(1);
}
// Getarnte Datei schreiben.
fs.mkdirSync(uploadsDir(), { recursive: true });
fs.writeFileSync(absoluteFilePath(), SVG_XSS, 'utf8');
// Test-Bankkarte anlegen oder wiederverwenden (idempotent, kein Duplikat).
const existing = await prisma.bankCard.findFirst({ where: { accountHolder: MARKER } });
const card = existing
? await prisma.bankCard.update({ where: { id: existing.id }, data: { customerId: customer.id, documentPath: RELATIVE_PATH } })
: await prisma.bankCard.create({
data: {
customerId: customer.id,
accountHolder: MARKER,
iban: 'DE00000000000000000000',
documentPath: RELATIVE_PATH,
description: 'Pentest R152 getarnte Datei (SVG-als-PDF). Nach Test via cleanup entfernen.',
},
});
const head = Buffer.alloc(12);
const fd = fs.openSync(absoluteFilePath(), 'r');
fs.readSync(fd, head, 0, 12, 0);
fs.closeSync(fd);
console.log('=== Magic-Byte-Test angelegt ===');
console.log('Test-Customer :', customer.customerNumber, `(id ${customer.id})`);
console.log('Test-Bankkarte : id', card.id, ` accountHolder="${MARKER}"`);
console.log('Datei (Disk) :', absoluteFilePath());
console.log('Erste 12 Bytes :', JSON.stringify(head.toString('latin1')), '(kein PDF/PNG/JPEG/GIF/WebP-Magic)');
console.log('documentPath :', RELATIVE_PATH);
console.log('');
console.log('--- So testen (Token des berechtigten Users anhängen) ---');
console.log(` curl -sSI "https://<host>/api/files/download?path=${RELATIVE_PATH}&disposition=inline&token=<JWT>"`);
console.log(' Erwartet: 200, Content-Disposition: attachment, X-Content-Type-Options: nosniff');
console.log(' (NICHT inline) + Backend-Log "Magic-Byte-Check fehlgeschlagen".');
console.log(` Ohne &disposition=inline: ebenfalls attachment.`);
console.log(' Als fremder Portal-User: 403 (Ownership-Check).');
console.log('');
console.log('>>> Nach dem Test aufräumen: npx tsx scripts/seed-magic-byte-test.ts cleanup');
}
async function cleanup(): Promise<void> {
let removed = 0;
const cards = await prisma.bankCard.findMany({ where: { accountHolder: MARKER } });
for (const c of cards) {
await prisma.bankCard.delete({ where: { id: c.id } });
removed++;
}
// Datei löschen (falls vorhanden).
const abs = absoluteFilePath();
let fileDeleted = false;
if (fs.existsSync(abs)) {
fs.unlinkSync(abs);
fileDeleted = true;
}
console.log('=== Cleanup ===');
console.log('Gelöschte Test-Bankkarten:', removed);
console.log('Datei gelöscht :', fileDeleted ? abs : '(nicht vorhanden)');
}
(async () => {
const mode = process.argv[2] || 'create';
const custIdx = process.argv.indexOf('--customer');
const customerIdArg = custIdx >= 0 ? parseInt(process.argv[custIdx + 1], 10) : undefined;
try {
if (mode === 'create') {
await create(Number.isFinite(customerIdArg as number) ? customerIdArg : undefined);
} else if (mode === 'cleanup') {
await cleanup();
} else {
console.error(`Unbekannter Modus "${mode}". Nutze: create | cleanup`);
process.exit(1);
}
} catch (err) {
console.error('Fehler:', err instanceof Error ? err.message : err);
process.exit(1);
} finally {
await prisma.$disconnect();
}
})();