/* ===================================================================== test-webdesign-gate.mjs — Selbsttest der Webdesign-Zugangsschranke. Ausführen (aus dem Ordner server/): node test-webdesign-gate.mjs Startet einen eigenen Server auf einem freien Port mit Test-Codes und spielt die sicherheitskritischen Fälle durch. Rührt weder die echte .env noch den laufenden Dienst an. Warum ein eigener Test und nicht "einmal im Browser klicken": Die gefährlichen Fälle sind genau die, die man beim Klicken NICHT sieht — ein Universe-Cookie, das hier akzeptiert wird; ein "next", das auf eine fremde Domain zeigt; eine Sperre, die nicht greift. ===================================================================== */ process.env.SITE_ACCESS_SECRET = "test-geheimnis-nur-fuer-diesen-lauf"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; process.env.WEBDESIGN_STATUS = "vorabpruefung"; // Wichtig für den Kernfall: das Universe ist öffentlich, /webdesign nicht. process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const { default: express } = await import("express"); const { default: cookieParser } = await import("cookie-parser"); const { webdesignGate, wdSigniere, wdPruefe } = await import("./webdesign-gate.js"); const { signSession } = await import("./gate.js"); const app = express(); app.set("trust proxy", 1); app.use(cookieParser()); app.use(express.json()); app.use(webdesignGate); // Ersatz für express.static — antwortet nur, wenn die Schranke durchlässt. app.use((req, res) => res.status(200).send("GEHEIMER INHALT")); const server = app.listen(0, "127.0.0.1"); await new Promise((r) => server.once("listening", r)); const basis = `http://127.0.0.1:${server.address().port}`; let bestanden = 0; let gescheitert = 0; function pruefe(name, bedingung, zusatz = "") { if (bedingung) { bestanden++; console.log(` ok ${name}`); } else { gescheitert++; console.log(` FEHLT ${name} ${zusatz}`); } } async function hole(pfad, optionen = {}) { const res = await fetch(basis + pfad, { redirect: "manual", ...optionen }); const text = await res.text(); let json = null; try { json = JSON.parse(text); } catch { /* kein JSON, egal */ } return { res, text, json, cookies: res.headers.getSetCookie?.() || [] }; } function cookieAus(cookies, name) { const treffer = cookies.find((c) => c.startsWith(name + "=")); return treffer ? treffer.split(";")[0].split("=").slice(1).join("=") : null; } console.log("\n=== Webdesign-Zugangsschranke ===\n"); /* --- 1. Der Kernfall: Universe ist öffentlich, /webdesign trotzdem dicht --- */ console.log("1. Grundschutz"); { const a = await hole("/webdesign/"); pruefe("ohne Code -> Umleitung zur Zugangswand", a.res.status === 302, `(war ${a.res.status})`); pruefe("Umleitungsziel ist die Zugangswand", (a.res.headers.get("location") || "").startsWith("/webdesign/zugang.html")); pruefe("Inhalt wird NICHT mitgeschickt", !a.text.includes("GEHEIMER INHALT")); const b = await hole("/webdesign/preise.html"); pruefe("auch Unterseiten sind dicht", b.res.status === 302); const c = await hole("/webdesign/zugang.html"); pruefe("Zugangswand selbst ist erreichbar", c.res.status === 200); const d = await hole("/index.html"); pruefe("Universe bleibt unberührt öffentlich", d.res.status === 200); const e = await hole("/webdesign/"); pruefe("noindex-Header ist gesetzt", (e.res.headers.get("x-robots-tag") || "").includes("noindex")); } /* --- 2. Anmeldung --- */ console.log("\n2. Anmeldung"); let sitzungDogi = null; { const falsch = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "irgendwas" }), }); pruefe("falscher Code -> 401", falsch.res.status === 401); const richtig = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "DOGI-TEST-1" }), }); pruefe("richtiger Code -> ok", richtig.res.status === 200 && richtig.json?.ok === true); pruefe("Rolle wird gemeldet", richtig.json?.rolle === "dogfather"); sitzungDogi = cookieAus(richtig.cookies, "dogiwd_session"); pruefe("Sitzungs-Cookie wird gesetzt", !!sitzungDogi); pruefe("Cookie ist httpOnly", richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && /httponly/i.test(c))); pruefe("Cookie ist secure", richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && /secure/i.test(c))); pruefe("Cookie hat KEIN Ablaufdatum (nur Browser-Sitzung)", richtig.cookies.some((c) => c.startsWith("dogiwd_session=") && !/max-age|expires/i.test(c))); const vanvan = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "VANVAN-TEST-1" }), }); pruefe("VanVans Code funktioniert gleichwertig", vanvan.json?.rolle === "vanvan"); const mit = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${sitzungDogi}` } }); pruefe("mit gültiger Sitzung -> Inhalt kommt", mit.res.status === 200 && mit.text.includes("GEHEIMER INHALT")); } /* --- 3. Weiterleitungsziel kann nicht auf eine fremde Seite zeigen --- */ console.log("\n3. Weiterleitungsziel"); { const faelle = [ ["//boese-seite.de", "protokoll-relative fremde Domain"], ["https://boese-seite.de", "vollstaendige fremde URL"], ["/webdesign/../../etc/passwd", "Pfadwechsel nach oben"], ["/index.html", "ausserhalb des Bereichs"], ["/webdesign\\boese", "Rueckwaertsschraegstrich"], ]; for (const [wert, was] of faelle) { const a = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "DOGI-TEST-1", next: wert }), }); pruefe(`abgewehrt: ${was}`, a.json?.next === "/webdesign/", `(wurde ${a.json?.next})`); } const gut = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "DOGI-TEST-1", next: "/webdesign/preise.html" }), }); pruefe("eigener Pfad wird uebernommen", gut.json?.next === "/webdesign/preise.html"); } /* --- 4. Ein Universe-Cookie darf hier NICHT gelten --- */ console.log("\n4. Trennung vom Universe-Zugang"); { const universeToken = await signSession("dogi", process.env.SITE_ACCESS_SECRET); const a = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${universeToken}` } }); pruefe("Universe-Sitzung oeffnet den Webdesign-Bereich NICHT", a.res.status === 302); pruefe("wdPruefe lehnt Universe-Token ab", wdPruefe(universeToken, process.env.SITE_ACCESS_SECRET) === null); const fremd = wdSigniere("dogfather", "ein-ganz-anderes-geheimnis"); const b = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${fremd}` } }); pruefe("mit falschem Geheimnis signiert -> abgelehnt", b.res.status === 302); const verdreht = sitzungDogi.split(".")[0] + "." + "AAAA"; const c = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${verdreht}` } }); pruefe("verfaelschte Signatur -> abgelehnt", c.res.status === 302); const abgelaufen = (() => { const inhalt = Buffer.from(JSON.stringify({ bereich: "webdesign", rolle: "dogfather", exp: Date.now() - 1000 })) .toString("base64url"); // absichtlich ohne gueltige Signatur -- muss doppelt scheitern return inhalt + ".xxx"; })(); const d = await hole("/webdesign/", { headers: { Cookie: `dogiwd_session=${abgelaufen}` } }); pruefe("abgelaufene/ungueltige Sitzung -> abgelehnt", d.res.status === 302); } /* --- 5. Fehlversuchs-Sperre --- */ console.log("\n5. Fehlversuchs-Sperre"); { let letzte = null; for (let i = 0; i < 6; i++) { letzte = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "falsch-" + i }), }); } pruefe("nach fuenf Fehlversuchen greift die Sperre (429)", letzte.res.status === 429, `(war ${letzte.res.status})`); const trotzRichtig = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code: "DOGI-TEST-1" }), }); pruefe("Sperre gilt auch fuer den richtigen Code", trotzRichtig.res.status === 429); pruefe("Meldung nennt die Wartezeit", /Minute/.test(trotzRichtig.json?.error || "")); const andere = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json", "CF-Connecting-IP": "203.0.113.77" }, body: JSON.stringify({ code: "DOGI-TEST-1" }), }); pruefe("andere Besucher sind NICHT mitgesperrt", andere.res.status === 200, "(genau dieser Fehler trat am 19.08.2026 im Universe auf)"); } /* --- 6. Statusmodi --- */ console.log("\n6. Zugangsstatus"); { process.env.WEBDESIGN_STATUS = "entwurf"; const a = await hole("/webdesign/zugang-pruefen", { method: "POST", headers: { "Content-Type": "application/json", "CF-Connecting-IP": "203.0.113.90" }, body: JSON.stringify({ code: "DOGI-TEST-1" }), }); pruefe("ENTWURF: auch der richtige Code oeffnet nicht", a.res.status === 403); process.env.WEBDESIGN_STATUS = "freigegeben"; const b = await hole("/webdesign/preise.html"); pruefe("FREIGEGEBEN: oeffentliche Seite ohne Code erreichbar", b.res.status === 200); const c = await hole("/webdesign/verwaltung.html"); pruefe("FREIGEGEBEN: Verwaltung bleibt trotzdem geschuetzt", c.res.status === 302); const d = await hole("/webdesign/preise.html"); pruefe("FREIGEGEBEN: kein noindex mehr", !(d.res.headers.get("x-robots-tag") || "").includes("noindex")); process.env.WEBDESIGN_STATUS = "vorabpruefung"; } /* --- 7. Ohne Geheimnis muss zugesperrt werden, nicht aufgesperrt --- */ console.log("\n7. Fehlende Einstellung"); { const gemerkt = process.env.SITE_ACCESS_SECRET; delete process.env.SITE_ACCESS_SECRET; const a = await hole("/webdesign/"); pruefe("ohne Geheimnis -> 503 statt offener Tuer", a.res.status === 503, `(war ${a.res.status})`); pruefe("Inhalt bleibt geheim", !a.text.includes("GEHEIMER INHALT")); process.env.SITE_ACCESS_SECRET = gemerkt; } server.close(); console.log(`\n=== ${bestanden} bestanden, ${gescheitert} gescheitert ===\n`); process.exit(gescheitert === 0 ? 0 : 1);