/** * Upload-Whitelist Suite * * Verifiziert das serverseitige Whitelist-Gate `assertAllowedUploadKind`: * Nur ausdrücklich erlaubte Dateitypen (Belege: PDF/JPG/PNG/WebP/HEIC, Profilbilder: * JPG/PNG/WebP) werden akzeptiert — entschieden wird ausschließlich über die * Magic Bytes des Buffers. Der deklarierte MIME-Type ist vom Client steuerbar * und darf NIE Zugriff gewähren. Kein beliebiger Dateityp darf die * Verarbeitungspipeline erreichen. */ import { describe, test, expect } from "./runner"; import { AVATAR_ALLOWED_KINDS, RECEIPT_ALLOWED_KINDS, UnsupportedFileTypeError, assertAllowedUploadKind, } from "../../src/lib/ingest/acceptedTypes"; function bufferFromHex(hex: string): Buffer { return Buffer.from(hex.replace(/\s+/g, ""), "hex"); } /* ----------------------- Echte Magic-Byte-Buffer (Inline) ------------------ */ // JPEG: FF D8 FF const JPEG_BUFFER = bufferFromHex("FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 01 00 01 00 00"); // PNG: 89 50 4E 47 0D 0A 1A 0A const PNG_BUFFER = bufferFromHex( "89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 01 00 00 00 01 08 06 00 00 00 1F 15 C4 89" ); // WebP: RIFF....WEBP const WEBP_BUFFER = Buffer.from("RIFF" + "\x10\x00\x00\x00" + "WEBPVP8 ", "latin1"); // PDF: %PDF- im Kopf const PDF_BUFFER = Buffer.from( "%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog >>\nendobj\n", "latin1" ); // Erkannte, aber für Belege NICHT erlaubte Typen: const GIF_BUFFER = Buffer.from("GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00", "latin1"); const TIFF_BUFFER = bufferFromHex("49 49 2A 00 08 00 00 00 00 00 00 00"); const BMP_BUFFER = bufferFromHex("42 4D 36 00 00 00 00 00 00 00 36 00 00 00"); const AVIF_BUFFER = Buffer.from("\x00\x00\x00\x1Cftypavif\x00\x00\x00\x00", "latin1"); const HEIC_BUFFER = Buffer.from("\x00\x00\x00\x18ftypheic\x00\x00\x00\x00", "latin1"); // Keine (erkennbare) Signatur: const TEXT_BUFFER = Buffer.from("Dies ist nur Textinhalt und keine Datei.", "utf8"); // MZ-Header (Windows-EXE) — von der Erkennung nicht als zulässiger Typ klassifiziert: const EXE_BUFFER = bufferFromHex("4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00"); const RECEIPT_MESSAGE = "Dateityp nicht erlaubt. Erlaubt sind: PDF, JPG, PNG, WebP, HEIC."; const AVATAR_MESSAGE = "Dateityp nicht erlaubt. Erlaubt sind: JPG, PNG, WebP."; function captureError(fn: () => unknown): unknown { try { fn(); return null; } catch (err) { return err; } } /* -------------------------------------------------------------------------- */ describe("UploadWhitelist — Beleg-Scans (PDF/JPG/PNG/WebP/HEIC)", () => { test("JPEG-Magic-Bytes werden akzeptiert", () => { expect(assertAllowedUploadKind(JPEG_BUFFER, "image/jpeg", RECEIPT_ALLOWED_KINDS)).toBe( "jpeg" ); }); test("PNG-Magic-Bytes werden akzeptiert", () => { expect(assertAllowedUploadKind(PNG_BUFFER, "image/png", RECEIPT_ALLOWED_KINDS)).toBe("png"); }); test("WebP-Magic-Bytes werden akzeptiert", () => { expect(assertAllowedUploadKind(WEBP_BUFFER, "image/webp", RECEIPT_ALLOWED_KINDS)).toBe( "webp" ); }); test("PDF-Magic-Bytes werden akzeptiert", () => { expect(assertAllowedUploadKind(PDF_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS)).toBe( "pdf" ); }); test("deklarierter MIME-Type ist optional (Magic Bytes allein entscheiden)", () => { expect(assertAllowedUploadKind(JPEG_BUFFER, undefined, RECEIPT_ALLOWED_KINDS)).toBe("jpeg"); }); test("deklarierter MIME-Type kann eine echte Datei nicht ausbremsen", () => { // Echte JPEG-Signatur, aber unplausibler/geloggener MIME-Type → Magic Bytes gewinnen. expect( assertAllowedUploadKind(JPEG_BUFFER, "application/octet-stream", RECEIPT_ALLOWED_KINDS) ).toBe("jpeg"); expect( assertAllowedUploadKind(PDF_BUFFER, "text/plain", RECEIPT_ALLOWED_KINDS) ).toBe("pdf"); }); test("deklarierter MIME-Type kann eine echte Datei nicht auf einen anderen erlaubten Typ umbiegen", () => { // JPEG-Signatur mit deklariertem application/pdf → weiterhin "jpeg", nicht "pdf". expect( assertAllowedUploadKind(JPEG_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS) ).toBe("jpeg"); }); test("GIF wird abgelehnt (erkannt, aber nicht in der Beleg-Whitelist)", () => { const err = captureError(() => assertAllowedUploadKind(GIF_BUFFER, "image/gif", RECEIPT_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("gif"); expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE); }); test("HEIC-Magic-Bytes werden akzeptiert", () => { expect(assertAllowedUploadKind(HEIC_BUFFER, "image/heic", RECEIPT_ALLOWED_KINDS)).toBe("heic"); }); test("TIFF/BMP/AVIF werden abgelehnt (erkannt, aber nicht in der Beleg-Whitelist)", () => { const cases: Array<[Buffer, string, string]> = [ [TIFF_BUFFER, "image/tiff", "tiff"], [BMP_BUFFER, "image/bmp", "bmp"], [AVIF_BUFFER, "image/avif", "avif"], ]; for (const [buffer, mime, kind] of cases) { const err = captureError(() => assertAllowedUploadKind(buffer, mime, RECEIPT_ALLOWED_KINDS)); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe(kind); expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE); } }); test("Text-Buffer ohne Signatur wird abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(TEXT_BUFFER, "text/plain", RECEIPT_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("unknown"); expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE); }); test("EXE-Buffer (MZ-Header) wird abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(EXE_BUFFER, "application/octet-stream", RECEIPT_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("unknown"); }); test("gespoofte deklarierte MIME-Types gewähren nie Zugriff", () => { // Text-Inhalt, deklariert als image/jpeg → abgelehnt. const jpegSpoof = captureError(() => assertAllowedUploadKind(TEXT_BUFFER, "image/jpeg", RECEIPT_ALLOWED_KINDS) ); expect(jpegSpoof).toBeInstanceOf(UnsupportedFileTypeError); // Text-Inhalt, deklariert als application/pdf → abgelehnt (kein Zugriff über // den deklarierten Typ — nur die Magic Bytes entscheiden). const pdfSpoof = captureError(() => assertAllowedUploadKind(TEXT_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS) ); expect(pdfSpoof).toBeInstanceOf(UnsupportedFileTypeError); expect((pdfSpoof as UnsupportedFileTypeError).kind).toBe("unknown"); }); test("leerer Buffer wird abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(Buffer.alloc(0), "image/jpeg", RECEIPT_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); }); }); describe("UploadWhitelist — Profilbilder (JPG/PNG/WebP, kein PDF)", () => { test("JPEG/PNG/WebP werden für Profilbilder akzeptiert", () => { expect(assertAllowedUploadKind(JPEG_BUFFER, "image/jpeg", AVATAR_ALLOWED_KINDS)).toBe( "jpeg" ); expect(assertAllowedUploadKind(PNG_BUFFER, "image/png", AVATAR_ALLOWED_KINDS)).toBe("png"); expect(assertAllowedUploadKind(WEBP_BUFFER, "image/webp", AVATAR_ALLOWED_KINDS)).toBe( "webp" ); }); test("PDF wird für Profilbilder abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(PDF_BUFFER, "application/pdf", AVATAR_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("pdf"); expect((err as UnsupportedFileTypeError).message).toBe(AVATAR_MESSAGE); }); test("GIF wird für Profilbilder abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(GIF_BUFFER, "image/gif", AVATAR_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("gif"); }); test("Text-Inhalt mit deklariertem image/png wird auch für Profilbilder abgelehnt", () => { const err = captureError(() => assertAllowedUploadKind(TEXT_BUFFER, "image/png", AVATAR_ALLOWED_KINDS) ); expect(err).toBeInstanceOf(UnsupportedFileTypeError); expect((err as UnsupportedFileTypeError).kind).toBe("unknown"); }); });