diff --git a/pruef-admin-schutz.mjs b/pruef-admin-schutz.mjs new file mode 100644 index 00000000..8d0d5922 --- /dev/null +++ b/pruef-admin-schutz.mjs @@ -0,0 +1,95 @@ +/* ===================================================================== + Sind ALLE Admin-Routen wirklich geschützt? + + WARUM AUTOMATISCH AUS DEM CODE GELESEN + + Der Schutz der Verwaltungs-Endpunkte sitzt heute in jedem einzelnen + Handler, nicht als eine Sperre vor der ganzen Gruppe. Das funktioniert + -- solange niemand beim Hinzufügen einer neuen Route den Aufruf + vergisst. Genau dieser Fehler fällt beim Klicken nicht auf: Die neue + Route tut, was sie soll, sie ist nur zusätzlich für alle offen. + + Ein Test mit einer von Hand gepflegten Routenliste hülfe hier nicht -- + man würde dieselbe Zeile vergessen, die man auch beim Absichern + vergessen hat. Deshalb liest dieser Test die Routen direkt aus + index.js. Eine künftig hinzugefügte, ungeschützte Admin-Route taucht + damit automatisch als Fehlschlag auf, ohne dass jemand den Test pflegt. + + WARUM MIT EINEM UNGÜLTIGEN TOKEN + + Getestet wird gegen die echte, laufende Domain. Ein ungültiges + Bearer-Token ist der gefahrlose Weg, das auch für schreibende Routen + (anlegen, ablehnen, löschen) zu tun: Eine korrekt geschützte Route + lehnt das Token ab, BEVOR ihr Handler auch nur beginnt. Es kann also + nichts angelegt, geändert oder gelöscht werden. Kommt statt der + Ablehnung eine inhaltliche Antwort, ist genau das der Fund. + + Aufruf: node pruef-admin-schutz.mjs + ===================================================================== */ +import { readFileSync } from "node:fs"; + +const BASIS = "https://postfach.dogfather-universe.com"; +const INDEX = "server-internal/index.js"; + +/* Alle Routen aus index.js ziehen: app.get("...") / app.post("...") usw. + Danach auf Admin-Pfade eingrenzen. */ +const quelle = readFileSync(INDEX, "utf8"); +const muster = /app\.(get|post|put|patch|delete)\(\s*["'`]([^"'`]+)["'`]/g; +const routen = []; +for (const m of quelle.matchAll(muster)) { + const methode = m[1].toUpperCase(); + const pfad = m[2]; + if (/\/admin\//.test(pfad)) routen.push({ methode, pfad }); +} + +/* Platzhalter (:id, :kundeId ...) durch einen Wert ersetzen, der sicher + nirgends existiert -- die Route soll am fehlenden Token scheitern, nicht + an einer Objektsuche. */ +const konkret = (pfad) => pfad.replace(/:[A-Za-z]+/g, "nichtexistent000"); + +let ok = 0, fehl = 0; +const funde = []; +const pruefe = (r, code) => { + /* 401 (nicht angemeldet) und 403 (angemeldet, aber ohne Recht) gelten + beide als "geschützt". Alles andere -- 200, 400, 404, 500 -- bedeutet: + die Anfrage kam am Türsteher vorbei. */ + const geschuetzt = code === 401 || code === 403; + if (geschuetzt) { ok++; } + else { + fehl++; + funde.push(`${r.methode} ${r.pfad} -> ${code}`); + } + console.log(` ${geschuetzt ? "ok " : "FEHL"} ${String(code).padEnd(4)} ${r.methode.padEnd(6)} ${r.pfad}`); +}; + +console.log(`\n${routen.length} Admin-Routen aus index.js gelesen. Jede mit ungültigem Token:\n`); + +for (const r of routen) { + const url = BASIS + konkret(r.pfad); + try { + const antwort = await fetch(url, { + method: r.methode, + headers: { + authorization: "Bearer ungueltig-testtoken-000", + "content-type": "application/json", + }, + /* Leerer, gültiger JSON-Körper für schreibende Methoden, damit ein + etwaiger Body-Parser nicht schon vorher meckert -- wir wollen die + Token-Prüfung messen, nicht die Eingabevalidierung. */ + body: r.methode === "GET" ? undefined : "{}", + signal: AbortSignal.timeout(15000), + }); + pruefe(r, antwort.status); + } catch (e) { + fehl++; + funde.push(`${r.methode} ${r.pfad} -> keine Antwort (${String(e.message).slice(0, 30)})`); + console.log(` FEHL --- ${r.methode.padEnd(6)} ${r.pfad} (${String(e.message).slice(0, 30)})`); + } +} + +console.log(`\n===== ${ok} geschützt, ${fehl} auffällig =====`); +if (funde.length) { + console.log("\nAuffällige Routen (kamen am Token-Schutz vorbei):"); + for (const f of funde) console.log(" " + f); +} +process.exitCode = fehl ? 1 : 0;