Filipe: "kannst du bitte machen dass die leute sich nur einmal anmelden
muessen und dan nur noch abgemeldet werden wenn sie sich selbst
abmelden. damit sie auch die benarichtigungen sofort kriegen ... und
auch sofort die anrufe annehmen koennen."
---- 1. DIE ANMELDUNG BLEIBT ----------------------------------------
VORHER: zwoelf Stunden, feste Frist ab dem Anmelden. Wer morgens um
acht anfing, flog abends um acht raus -- mitten im Betrieb. Und wer
abgemeldet ist, hat die Seite nicht offen; ein Anruf erreicht ihn dann
nur noch ueber die Benachrichtigung, und bis er sich wieder angemeldet
hat, ist das Klingeln vorbei. Genau das beschreibt Filipe.
JETZT: ein gleitendes Fenster von 180 Tagen, das sich bei jeder Nutzung
verlaengert. Wer die Seite benutzt, bleibt angemeldet -- ohne Ende.
NICHT UNENDLICH, und das ist Absicht: In diesem Haus liegen
vertrauliche Meldungen ueber Menschen. Ein Zugang, der nie ablaeuft,
ist auf einem verlorenen Handy fuer immer offen. Ein halbes Jahr ohne
Besuch schliesst das Geraet und ist niemandem zu viel zugemutet.
An EINER Stelle gebaut: `sitzungLesen` -- durch die gehen alle 26
Fachmodule. Ein Parameter mehr haette 26 Aufrufe geaendert und beim 27.
Modul vergessen werden koennen; `req.res` haengt ohnehin an der
Anfrage. Verlaengert wird erst, wenn weniger als die Haelfte des
Fensters uebrig ist -- sonst waere das ein Schreibzugriff bei jedem
Bild und jedem Herzschlag des Ereignisstroms.
Drei Texte, die noch "zwoelf Stunden" behaupteten, sagen es jetzt
richtig -- inklusive der Abmelde-Nachfrage, die jetzt dazusagt, dass
man angemeldet bleiben SOLLTE, um Anrufe zu bekommen.
---- 2. BEIM ZURUECKGEHEN BLEIBT DIE STELLE -------------------------
Filipe, zum dritten Mal und in Grossbuchstaben. Also erst gemessen:
gescrollt auf: 900
gemerkt: 900
gelandet: 1054 <- 154 px daneben, zuverlaessig
Die Wiederherstellung LIEF also -- sie traf nur nicht. Ursache ist
Chromes Scroll-Verankerung: Waechst Inhalt OBERHALB der Stelle,
verschiebt der Browser den Bildlauf mit, damit das Sichtbare stehen
bleibt. Im Alltag genau richtig; beim Wiederherstellen das Gegenteil.
Sie wird jetzt fuer die Dauer des Wiederherstellens abgeschaltet und
danach wieder eingeschaltet -- nicht dauerhaft, sonst spraenge einem im
Chat der Text unter dem Finger weg. Dazu wird die Stelle nachgesetzt,
solange die Seite noch waechst, und aufgehoert, sobald sie 400 ms lang
ruhig ist. Ergebnis: 900 -> 900, und es bleibt dort.
DAZU, und das ist der groessere Teil: Der Ereignisstrom in kopf.js
laeuft auf 32 Seiten und wird jetzt beim Weggehen geschlossen. Eine
offene EventSource sperrt den Vor-/Zurueck-Speicher des Browsers aus --
deshalb wurde bisher JEDE Rueckkehr ein vollstaendiger Neuaufbau.
Filipe hat genau das beschrieben ("OHNE DASS DIE SEITE ... NEU LAEDT").
EHRLICH DAZU: Playwright schaltet diesen Speicher fuer Tests ab, und er
liess sich hier nicht einschalten. Ich kann also NICHT messen, dass er
jetzt greift -- die Aenderung ist trotzdem richtig (eine offene
Verbindung ist die dokumentierte Sperre, und ein Strom, der beim
Weggehen offen bleibt, ist ohnehin ein Zuhoerer, den niemand mehr
liest). Gemessen und abgesichert ist der Weg OHNE diesen Speicher --
also der schlechteste Fall.
---- 3. DABEI GEFUNDEN: pruef-schranke mass seit neun Tagen nichts --
Sie suchte `const GESCHUETZT = {` per Textmuster in workspace.js. Diese
Tabelle ist am 11.09.2026 nach rechte.js umgezogen -- seitdem fand das
Muster nichts, und die Pruefung meldete "0 geschuetzte Seiten", "alle 0
Seiten leiten um", "0 Schreibweisen ausprobiert". Drei rote Zeilen, die
nach einem Zaehlfehler aussahen und in Wahrheit hiessen: hier wird
nichts mehr geprueft.
Jetzt wird die Tabelle IMPORTIERT. Ein Textmuster auf fremden
Quelltext reisst beim naechsten Umzug still; ein import reisst laut.
Ergebnis: 33 geschuetzte Seiten, 363 Schreibweisen -- alles gruen.
GEPRUEFT: pruef-rollstelle 8/0 (neu, mit Spur ueber die Zeit und zwei
Gegenproben), pruef-schranke (war rot), pruef-code 17/0,
pruef-haerte 20/0.
Co-Authored-By: Claude Opus 5 <[email protected]>
398 lines
18 KiB
JavaScript
398 lines
18 KiB
JavaScript
/* KOMMT MAN AN DER SCHRANKE VORBEI?
|
|
|
|
Auftrag vom 04.09.2026 (vollstaendiger Audit): "pruefe direkte
|
|
URL-Aufrufe, Zugriff ohne Login, Zugriff mit falscher Rolle".
|
|
|
|
WARUM DIESE PRUEFUNG NOETIG IST. Der Seitenschutz in workspace.js
|
|
vergleicht `req.path` gegen eine Liste:
|
|
|
|
if (!Object.hasOwn(GESCHUETZT, req.path)) return next();
|
|
|
|
Ein EXAKTER Vergleich. Trifft er nicht, geht die Anfrage ungeprueft
|
|
weiter an express.static. Alles haengt damit an der Frage, ob es eine
|
|
Schreibweise derselben Datei gibt, die anders aussieht als der
|
|
Listeneintrag -- ein zweiter Schraegstrich, ein Punkt-Segment, eine
|
|
kodierte Form, eine andere Gross-/Kleinschreibung.
|
|
|
|
Das laesst sich nicht durch Lesen entscheiden: Ob Express den Pfad
|
|
vorher zurechtruecken, ob serve-static ihn nochmal aufloest, haengt an
|
|
den Fassungen im Einsatz. Also wird es AUSPROBIERT, gegen den echten
|
|
Server, mit den echten Fassungen.
|
|
|
|
ERWARTUNG je Aufruf: entweder Umleitung zur Anmeldung (302) oder
|
|
"gibt es nicht" (404). Was NICHT vorkommen darf, ist 200 mit Inhalt.
|
|
|
|
DIE GEGENPROBE steht am Ende: Mit gueltiger Anmeldung MUSS dieselbe
|
|
Seite mit 200 kommen. Sonst pruefte dieser Lauf nur, dass der Server
|
|
auf alles 404 sagt -- und waere wertlos. */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-schranke-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
/* Der Port muss frei sein -- sonst wird gegen einen fremden Server
|
|
gemessen. Begruendung in helfer-port.mjs. */
|
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
await portMussFreiSein(4231, "die Schrankenpruefung");
|
|
|
|
process.env.PORT = "4231";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
const express = (await import("express")).default;
|
|
const ec = express.response.cookie;
|
|
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
|
|
await import("./index.js");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = "http://127.0.0.1:4231";
|
|
|
|
let fehler = 0;
|
|
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---- Bestand anlegen ---------------------------------------------------- */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
|
|
} catch { await new Promise((r) => setTimeout(r, 250)); }
|
|
}
|
|
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
function anlegen(name, rolle, code) {
|
|
const salt = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768, jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
anlegen("Chef", "admin", "CODE-DOGI-0001");
|
|
anlegen("Mara", "manager", "CODE-MANA-0001");
|
|
anlegen("Pat", "scout", "CODE-SCOU-0001");
|
|
anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
d.close();
|
|
|
|
async function anmelden(rolle, code) {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }) });
|
|
const roh = a.headers.getSetCookie?.() || [];
|
|
const keks = roh.map((z) => z.split(";")[0]).join("; ");
|
|
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
|
return keks;
|
|
}
|
|
|
|
const kekse = {
|
|
admin: await anmelden("admin", "CODE-DOGI-0001"),
|
|
manager: await anmelden("manager", "CODE-MANA-0001"),
|
|
scout: await anmelden("scout", "CODE-SCOU-0001"),
|
|
creator: await anmelden("creator", "CODE-CREA-0001"),
|
|
};
|
|
|
|
/** Roh anfragen, ohne Umleitungen zu folgen -- die Umleitung IST die
|
|
* Antwort, der wir nachgehen. */
|
|
const hol = (pfad, keks) => fetch(BASIS + pfad, {
|
|
redirect: "manual", headers: keks ? { cookie: keks } : {} });
|
|
|
|
/* =======================================================================
|
|
1. Ohne Anmeldung an eine geschuetzte Seite
|
|
======================================================================= */
|
|
console.log("\n=== Ohne Anmeldung ===");
|
|
|
|
/* DIE LISTE WIRD IMPORTIERT, NICHT AUS DEM QUELLTEXT GEKRATZT
|
|
(berichtigt 20.09.2026).
|
|
|
|
Hier stand ein Textmuster auf "const GESCHUETZT = {" in
|
|
workspace.js. Diese Tabelle ist am 11.09.2026 nach rechte.js
|
|
UMGEZOGEN -- und seitdem fand das Muster nichts. Die Pruefung lief
|
|
weiter, meldete "0 geschuetzte Seiten" und pruefte damit neun Tage
|
|
lang NICHTS: keine Umleitung, keine Schreibweise, keine Schranke.
|
|
Drei rote Zeilen, die niemand las, weil sie nach einem kleinen
|
|
Zaehlfehler aussahen.
|
|
|
|
Gefunden beim Umbau der Anmeldedauer -- also zufaellig. Ein
|
|
Textmuster auf fremden Quelltext ist genau die Art Verbindung, die
|
|
beim naechsten Umzug still reisst. Ein import reisst laut. */
|
|
const { SEITEN: RECHTE_SEITEN } = await import("./rechte.js");
|
|
const SEITEN = Object.keys(RECHTE_SEITEN)
|
|
.filter((x) => /workspace/.test(x) && x.endsWith(".html"));
|
|
|
|
ok(SEITEN.length >= 15, `${SEITEN.length} geschuetzte Seiten aus rechte.js gelesen`);
|
|
|
|
let geprueft = 0;
|
|
for (const seite of SEITEN) {
|
|
const a = await hol(seite, null);
|
|
geprueft++;
|
|
if (a.status !== 302) ok(false, `${seite} ohne Anmeldung -> ${a.status} statt 302`);
|
|
}
|
|
ok(geprueft === SEITEN.length && SEITEN.length > 0,
|
|
`alle ${geprueft} Seiten leiten ohne Anmeldung zur Anmeldung um`);
|
|
|
|
/* =======================================================================
|
|
2. Andere Schreibweisen desselben Pfades
|
|
======================================================================= */
|
|
console.log("\n=== Andere Schreibweisen (Umgehungsversuche) ===");
|
|
|
|
/* Jede Variante muss dasselbe tun wie der schlichte Pfad: wegschicken
|
|
oder nicht finden. Ein 200 mit HTML waere die Luecke. */
|
|
const VARIANTEN = (s) => {
|
|
const name = s.replace("/workspace/", "");
|
|
return [
|
|
[`//workspace/${name}`, "zwei Schraegstriche vorn"],
|
|
[`/workspace//${name}`, "zwei Schraegstriche in der Mitte"],
|
|
[`/workspace/./${name}`, "Punkt-Segment"],
|
|
[`/workspace/x/../${name}`, "Umweg ueber .."],
|
|
[`/workspace/%2e/${name}`, "kodierter Punkt"],
|
|
[`/workspace/${name.toUpperCase()}`, "Grossschreibung"],
|
|
[`/workspace/${name}%20`, "angehaengtes Leerzeichen"],
|
|
[`/workspace/${name}.`, "angehaengter Punkt"],
|
|
[`/workspace/${encodeURIComponent(name)}`, "kodierter Name"],
|
|
[`/workspace/${name};x=1`, "Pfad-Parameter"],
|
|
[`/./workspace/${name}`, "Punkt vor workspace"],
|
|
];
|
|
};
|
|
|
|
let variantenGeprueft = 0;
|
|
const durchgerutscht = [];
|
|
for (const seite of SEITEN) {
|
|
for (const [pfad, wie] of VARIANTEN(seite)) {
|
|
let a;
|
|
try { a = await hol(pfad, null); } catch { continue; }
|
|
variantenGeprueft++;
|
|
if (a.status === 200) {
|
|
const text = await a.text();
|
|
/* Nur echter Seiteninhalt zaehlt -- eine leere 200 waere zwar
|
|
auch seltsam, aber kein Datenabfluss. */
|
|
if (/<html|<!doctype/i.test(text) && text.length > 500) {
|
|
durchgerutscht.push(`${pfad} (${wie}) -> 200, ${text.length} Zeichen`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ok(variantenGeprueft > 100, `${variantenGeprueft} Schreibweisen ausprobiert`);
|
|
ok(durchgerutscht.length === 0,
|
|
durchgerutscht.length
|
|
? `SCHRANKE UMGANGEN: ${durchgerutscht.slice(0, 6).join(" · ")}`
|
|
: "keine Schreibweise kommt an der Schranke vorbei");
|
|
|
|
/* =======================================================================
|
|
3. Falsche Rolle
|
|
======================================================================= */
|
|
console.log("\n=== Falsche Rolle ===");
|
|
|
|
/* Was jede Rolle NICHT sehen darf, steht ebenfalls in workspace.js.
|
|
Hier wird gegengeprueft, dass die Umleitung wirklich greift. */
|
|
/* PERSONEN.HTML IST SEIT DEM 07.09.2026 NICHT MEHR NUR FUER DOGFATHER.
|
|
Filipe: Manager sollen "creator und auch wirklich nur creator
|
|
hinzufuegen koennen" -- dafuer muessen sie die Seite oeffnen duerfen.
|
|
Sie steht deshalb nicht mehr in dieser Liste, sondern in der Liste
|
|
darunter, mit ihrer eigenen, engeren Erwartung. Automationen bleiben
|
|
allein bei DogFather.
|
|
|
|
Wichtig fuer die Aussagekraft: Die Seite VERSCHWINDET nicht aus der
|
|
Pruefung, sie wechselt die Erwartung. Haette ich sie nur
|
|
herausgenommen, waere die Zahl der Pruefungen gesunken und der
|
|
Manager-Weg ab da ungeprueft geblieben. */
|
|
const NUR_DOGFATHER = ["/workspace/automation.html"];
|
|
for (const seite of NUR_DOGFATHER) {
|
|
for (const rolle of ["manager", "scout", "creator"]) {
|
|
const a = await hol(seite, kekse[rolle]);
|
|
ok(a.status === 302, `${rolle} auf ${seite} -> ${a.status} (erwartet 302)`);
|
|
}
|
|
const a = await hol(seite, kekse.admin);
|
|
ok(a.status === 200, `DogFather auf ${seite} -> ${a.status} (erwartet 200)`);
|
|
}
|
|
|
|
/* Die Personenseite: Leitung ja, Scout und Creator nein. */
|
|
const NUR_LEITUNG = "/workspace/personen.html";
|
|
{
|
|
for (const rolle of ["scout", "creator"]) {
|
|
const a = await hol(NUR_LEITUNG, kekse[rolle]);
|
|
ok(a.status === 302, `${rolle} auf ${NUR_LEITUNG} -> ${a.status} (erwartet 302)`);
|
|
}
|
|
for (const rolle of ["admin", "manager"]) {
|
|
const a = await hol(NUR_LEITUNG, kekse[rolle]);
|
|
ok(a.status === 200, `${rolle} auf ${NUR_LEITUNG} -> ${a.status} (erwartet 200)`);
|
|
}
|
|
/* Und die Verwaltungsschnittstelle dahinter bleibt trotzdem zu:
|
|
Die Seite aufmachen zu duerfen heisst nicht, die Personenliste
|
|
lesen zu duerfen. Genau daran haengt, dass ein Manager nur
|
|
anlegen und nicht verwalten kann. */
|
|
const v = await hol("/workspace/api/verwaltung/personen", kekse.manager);
|
|
ok(v.status === 404, `aber die Verwaltungsdaten bleiben zu (${v.status}, erwartet 404)`);
|
|
}
|
|
|
|
const NUR_TEAM = "/workspace/scouting.html";
|
|
{
|
|
const a = await hol(NUR_TEAM, kekse.creator);
|
|
ok(a.status === 302, `Creator auf ${NUR_TEAM} -> ${a.status} (erwartet 302)`);
|
|
const b = await hol(NUR_TEAM, kekse.scout);
|
|
ok(b.status === 200, `Scout auf ${NUR_TEAM} -> ${b.status} (erwartet 200)`);
|
|
}
|
|
|
|
/* Und dieselben Umgehungsversuche MIT falscher Rolle -- ein Manager, der
|
|
personen.html ueber //workspace/ bekaeme, waere genauso ein Loch. */
|
|
console.log("\n=== Falsche Rolle + andere Schreibweise ===");
|
|
const rolleDurchgerutscht = [];
|
|
let rolleGeprueft = 0;
|
|
for (const seite of [...NUR_DOGFATHER, NUR_LEITUNG]) {
|
|
for (const [pfad, wie] of VARIANTEN(seite)) {
|
|
/* Fuer die Personenseite zaehlt hier nur der Creator -- der Manager
|
|
DARF sie inzwischen sehen, also waere ein 200 dort kein Loch. */
|
|
for (const rolle of seite === NUR_LEITUNG ? ["creator"] : ["manager", "creator"]) {
|
|
let a;
|
|
try { a = await hol(pfad, kekse[rolle]); } catch { continue; }
|
|
rolleGeprueft++;
|
|
if (a.status === 200) {
|
|
const text = await a.text();
|
|
if (/<html|<!doctype/i.test(text) && text.length > 500) {
|
|
rolleDurchgerutscht.push(`${rolle}: ${pfad} (${wie})`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/* DIE ZAHL WIRD AUSGERECHNET, NICHT ABGESCHRIEBEN. Vorher stand hier
|
|
"> 40" -- eine Zahl aus dem Moment, in dem zwei Seiten mal zwei
|
|
Rollen geprueft wurden. Als die Personenseite fuer Manager aufging
|
|
und dort nur noch der Creator zu pruefen war, fielen es 33, und die
|
|
Pruefung wurde rot, ohne dass ein Weg ungeprueft geblieben waere.
|
|
Jetzt wird die Erwartung aus denselben Listen gerechnet, aus denen
|
|
auch geprueft wird: Sie stimmt automatisch, und ein WIRKLICHER
|
|
Ausfall (eine Schleife, die nicht laeuft) faellt trotzdem auf. */
|
|
const erwarteteVersuche = VARIANTEN(NUR_DOGFATHER[0]).length * NUR_DOGFATHER.length * 2
|
|
+ VARIANTEN(NUR_LEITUNG).length;
|
|
ok(rolleGeprueft === erwarteteVersuche && rolleGeprueft > 25,
|
|
`${rolleGeprueft} Versuche mit falscher Rolle (erwartet ${erwarteteVersuche})`);
|
|
ok(rolleDurchgerutscht.length === 0,
|
|
rolleDurchgerutscht.length
|
|
? `ROLLE UMGANGEN: ${rolleDurchgerutscht.slice(0, 6).join(" · ")}`
|
|
: "auch mit anderer Schreibweise kommt keine falsche Rolle durch");
|
|
|
|
/* =======================================================================
|
|
4. Die Datenbank und die Ablagen sind nicht aus dem Netz erreichbar
|
|
======================================================================= */
|
|
console.log("\n=== Nicht ausliefern, was nicht ins Netz gehoert ===");
|
|
for (const pfad of [
|
|
"/server/workspace.js", "/server/.env", "/.git/config", "/server/gate.js",
|
|
"/workspace-daten/workspace.db", "/CLAUDE.md", "/DEPLOY.md",
|
|
"/../workspace-daten/workspace.db", "/workspace/../server/workspace.js",
|
|
]) {
|
|
const a = await hol(pfad, kekse.admin);
|
|
const text = a.status === 200 ? await a.text() : "";
|
|
ok(a.status !== 200 || text.length < 200,
|
|
`${pfad} -> ${a.status}${a.status === 200 ? ` (${text.length} Zeichen!)` : ""}`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. Abgelaufene und gefaelschte Sitzung
|
|
======================================================================= */
|
|
console.log("\n=== Sitzung ===");
|
|
{
|
|
const a = await hol("/workspace/start.html", "dfw_sitzung=" + "a".repeat(64));
|
|
ok(a.status === 302, `erfundener Sitzungsschluessel -> ${a.status} (erwartet 302)`);
|
|
|
|
const b = await fetch(BASIS + "/workspace/api/aufgaben",
|
|
{ headers: { cookie: "dfw_sitzung=" + "b".repeat(64) } });
|
|
ok(b.status === 401, `erfundener Schluessel an der Schnittstelle -> ${b.status} (erwartet 401)`);
|
|
|
|
/* Eine Sitzung, deren Gueltigkeit abgelaufen ist, muss sterben --
|
|
auch wenn der Schluessel echt ist. */
|
|
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d2.prepare("UPDATE sitzungen SET gueltig_bis = ? WHERE person_id = (SELECT id FROM personen WHERE rolle='creator')")
|
|
.run("2020-01-01T00:00:00.000Z");
|
|
d2.close();
|
|
const c = await hol("/workspace/start.html", kekse.creator);
|
|
ok(c.status === 302, `abgelaufene Sitzung -> ${c.status} (erwartet 302)`);
|
|
|
|
/* Und eine gesperrte Person kommt mit gueltiger Sitzung nicht mehr rein. */
|
|
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
d3.prepare("UPDATE personen SET aktiv = 0 WHERE rolle = 'scout'").run();
|
|
d3.close();
|
|
const e = await hol("/workspace/start.html", kekse.scout);
|
|
ok(e.status === 302, `gesperrte Person mit gueltiger Sitzung -> ${e.status} (erwartet 302)`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. Sicherheits-Kopfzeilen
|
|
======================================================================= */
|
|
console.log("\n=== Sicherheits-Kopfzeilen ===");
|
|
{
|
|
const a = await hol("/workspace/", null);
|
|
for (const [name, erwartet] of [
|
|
["x-content-type-options", "nosniff"],
|
|
["x-frame-options", "SAMEORIGIN"],
|
|
["referrer-policy", "strict-origin-when-cross-origin"],
|
|
]) {
|
|
ok(a.headers.get(name) === erwartet,
|
|
`${name}: ${a.headers.get(name) || "fehlt"} (erwartet ${erwartet})`);
|
|
}
|
|
|
|
/* HSTS GEHOERT NICHT AUF EINE UNSICHERE VERBINDUNG (Befund 04.09.2026).
|
|
RFC 6797, 7.2: "An HSTS Host MUST NOT include the STS header field
|
|
in HTTP responses conveyed over non-secure transport."
|
|
|
|
Vorher ging der Header bei jeder Antwort mit. Live unauffaellig --
|
|
dort kommt alles ueber https. Aufgefallen im Browsertest: WebKit
|
|
nimmt ihn auch von einer http-Adresse an, merkt sich 127.0.0.1 als
|
|
"nur noch https", und danach scheitert jede Verbindung zum lokalen
|
|
Testserver mit "SSL connect error". Chromium und Firefox machen fuer
|
|
localhost eine Ausnahme, WebKit nicht -- also ausgerechnet der
|
|
Browser, den jedes iPhone benutzt, war damit nicht mehr pruefbar.
|
|
|
|
Node's fetch setzt den Host-Header selbst (verbotener Name), deshalb
|
|
hier ein roher Aufruf -- sonst misst man dreimal denselben Fall. */
|
|
const http = await import("node:http");
|
|
const mitHost = (host) => new Promise((fertig) => {
|
|
const r = http.request({ host: "127.0.0.1", port: 4231, path: "/404.html",
|
|
method: "GET", headers: { Host: host } }, (antwort) => {
|
|
antwort.resume();
|
|
fertig(antwort.headers["strict-transport-security"] || null);
|
|
});
|
|
r.on("error", () => fertig(null));
|
|
r.end();
|
|
});
|
|
|
|
ok((await mitHost("127.0.0.1:4231")) === null,
|
|
"ueber http an eine lokale Adresse kommt KEIN HSTS (RFC 6797)");
|
|
ok((await mitHost("localhost:4231")) === null,
|
|
"auch an localhost kommt kein HSTS");
|
|
const echt = await mitHost("dogfather-universe.com");
|
|
ok(!!echt && echt.includes("max-age="),
|
|
`an der echten Domain kommt HSTS weiterhin (${echt || "FEHLT"})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
GEGENPROBE — kann diese Pruefung ueberhaupt "erlaubt" sagen?
|
|
======================================================================= */
|
|
console.log("\n=== Gegenprobe ===");
|
|
{
|
|
const a = await hol("/workspace/start.html", kekse.admin);
|
|
const text = a.status === 200 ? await a.text() : "";
|
|
ok(a.status === 200 && text.length > 500,
|
|
`angemeldet kommt start.html normal an (${a.status}, ${text.length} Zeichen)`);
|
|
|
|
const b = await hol("/workspace/", null);
|
|
ok(b.status === 200, `die Anmeldeseite selbst bleibt offen (${b.status})`);
|
|
|
|
/* Und der Beweis, dass ein durchgerutschter Pfad ueberhaupt auffallen
|
|
WUERDE: eine ungeschuetzte Datei liefert echten Inhalt. */
|
|
const c = await hol("/workspace/assets/css/start.css", null);
|
|
const t = c.status === 200 ? await c.text() : "";
|
|
ok(c.status === 200 && t.length > 500,
|
|
`eine ungeschuetzte Datei kommt mit Inhalt (${c.status}, ${t.length} Zeichen) — die Messung kann also "durchgelassen" erkennen`);
|
|
}
|
|
|
|
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|