diff --git a/server/index.js b/server/index.js index 1f65f72d..bda1ec38 100644 --- a/server/index.js +++ b/server/index.js @@ -15,7 +15,7 @@ import { workspaceUmzug } from "./workspace-umzug.js"; import { crewWeiche } from "./crew-adresse.js"; import { webdesignGate } from "./webdesign-gate.js"; import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js"; -import { workspaceRouter, sichtSetzen } from "./workspace.js"; +import { workspaceRouter, sichtSetzen, gleicheHerkunft } from "./workspace.js"; import { aufgabenRouter } from "./workspace-aufgaben.js"; import { zuteilungRouter } from "./workspace-zuteilung.js"; import { materialRouter } from "./workspace-material.js"; @@ -219,6 +219,33 @@ app.use(webdesignGate); Das Modul ist so gebaut, dass es diese Website nicht mitreißen kann: Es öffnet beim Laden keine Datenbank, und jede Route fängt ihre Fehler selbst ab (Begründung im Kopf von workspace.js). */ +/* ==== KOMMT DIE ANFRAGE VON UNSERER EIGENEN SEITE? (07.10.2026) ====== + + Der Schutz gegen CSRF, für ALLE Wege unter /workspace/api -- an genau + einer Stelle und VOR allen Routern. + + WARUM NICHT IN DEN ROUTERN: Bis heute führten 35 Router je eine + eigene Abschrift dieser Prüfung, in drei verschiedenen + Formulierungen, und zehn hatten gar keine (anleitung, befinden, + buehne, hilfe, manager-ziele, material, reports, support, video, + zuteilung). `pruef-alle-wege` meldete davon vier -- nicht weil die + übrigen sicher waren, sondern weil sie auf die Probe-Nutzlast + zufällig 400 oder 404 antworten statt 200. + + UND WARUM NICHT „in jeden der zehn eine einsetzen": Genau das war + mein erster Versuch, und er hat etwas Unangenehmes gezeigt. Ein + `router.use("/workspace/api", …)` greift für JEDEN Weg mit diesem + Präfix, auch für die der anderen Router -- Express geht die Router + der Reihe nach durch, und der erste mit so einer Zeile schützt alles + dahinter. Die Absicherung hing damit an der Einhäng-REIHENFOLGE, + nicht an einer Absicht. Gemerkt habe ich es an der Gegenprobe: Ich + habe die Zeile aus `workspace-anleitung.js` wieder entfernt, und die + Prüfung blieb grün. Eine Absicherung, deren Fehlen keine Prüfung + bemerkt, ist keine. + + Also eine Zeile, hier, wo die Reihenfolge ausgesprochen ist. */ +app.use("/workspace/api", gleicheHerkunft); + app.use(workspaceRouter); /* DIE SICHT EINES ANDEREN (01.09.2026) -- setzt req.sicht. diff --git a/server/pruef-zentrale-ring.mjs b/server/pruef-zentrale-ring.mjs index 10db7354..ba66272c 100644 --- a/server/pruef-zentrale-ring.mjs +++ b/server/pruef-zentrale-ring.mjs @@ -134,14 +134,81 @@ for (let i = 0; i < 3; i++) { d.close(); -async function ring(code) { - const an = await fetch(BASIS + "/workspace/api/anmelden", { - method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ rolle: "creator", code }), +/* ===================================================================== + DIE ANMELDUNG WAR SEIT DEM 01.10.2026 KAPUTT (repariert 07.10.2026) + ===================================================================== + + Hier stand `rolle: "creator"` -- die drei Testleute weiter oben sind + aber als `modi` angelegt. Das ging jahrelang gut, weil der stille + Zugang JEDE Kachel durchliess. Seit die Kachel bindend ist, wird die + Anmeldung abgewiesen, und `/workspace/api/zentrale` antwortete + `{"fehler":"nicht_angemeldet"}`. + + WAS DANN PASSIERTE, IST DER EIGENTLICHE SCHADEN: Die Pruefung las + `r.titel` und `r.prozent` aus einer Fehlermeldung -- also `undefined`. + Dreizehn Zeilen wurden rot, und sie zeigten alle auf den Ring, der + voellig in Ordnung war. Eine davon wurde sogar GRUEN: + `r.prozent !== 20 && r.prozent !== 40` stimmt fuer `undefined` + natuerlich -- ein Haken, der nichts geprueft hat. + + ZWEI DINGE SIND NOETIG, und beide aus demselben Grund (die Kachel + gilt): + 1. Die ROLLE muss stimmen -- `modi`, nicht `creator`. + 2. Die WAND muss stimmen -- die Modi-Kachel steht auf der + Crew-Adresse, nicht auf der Agenturwand. + + UND DESHALB node:http STATT fetch: `fetch` (undici) setzt den + Host-Kopf selbst aus der Adresse und wirft einen mitgegebenen weg. + Mit fetch laesst sich die Crew-Wand gar nicht ansprechen -- genau + diese Stelle steht seit dem 22.09. auch in pruef-chat-kanaele und + pruef-modi-verborgen. */ +const { request: httpAnfrage } = await import("node:http"); +const CREW_WAND = "crew.dogfather-universe.com"; + +function anAlsModi(pfad, art, rumpfObjekt, keks) { + return new Promise((fertig, schief) => { + const rumpf = rumpfObjekt ? JSON.stringify(rumpfObjekt) : null; + const a = httpAnfrage({ + host: "127.0.0.1", port: PORT, path: pfad, method: art, + headers: { + Host: CREW_WAND, + ...(keks ? { Cookie: keks } : {}), + ...(rumpf ? { "Content-Type": "application/json", + "Content-Length": Buffer.byteLength(rumpf) } : {}), + }, + }, (antwort) => { + let text = ""; + antwort.on("data", (t) => { text += t; }); + antwort.on("end", () => fertig({ + code: antwort.statusCode, + kekse: [].concat(antwort.headers["set-cookie"] || []) + .map((z) => z.split(";")[0]).join("; "), + daten: (() => { try { return JSON.parse(text); } catch { return null; } })(), + })); + }); + a.on("error", schief); + if (rumpf) a.write(rumpf); + a.end(); }); - const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; "); - const a = await fetch(BASIS + "/workspace/api/zentrale", { headers: { cookie: keks } }); - return a.json(); +} + +async function ring(code) { + const an = await anAlsModi("/workspace/api/anmelden", "POST", { rolle: "modi", code }); + /* DER DRITTE AUSGANG: Scheitert die Anmeldung, ist das KEIN Befund + am Ring -- und genau so soll es dastehen. Sonst sucht beim + naechsten Mal wieder jemand dreizehn Fehler an einer Stelle, an + der keiner ist. */ + if (an.code !== 200 || !an.kekse) { + throw new Error(`KONNTE NICHT NACHSEHEN: Anmeldung als Modi misslang ` + + `(HTTP ${an.code}, ${JSON.stringify(an.daten)}). ` + + `Die Kachel gilt seit dem 01.10.2026 -- Rolle und Wand muessen zusammenpassen.`); + } + const a = await anAlsModi("/workspace/api/zentrale", "GET", null, an.kekse); + if (a.code !== 200) { + throw new Error(`KONNTE NICHT NACHSEHEN: /api/zentrale antwortete ${a.code} ` + + `(${JSON.stringify(a.daten)})`); + } + return a.daten; } /* ======================================================================= diff --git a/server/workspace-buehne.js b/server/workspace-buehne.js index 86f072fe..23070ee2 100644 --- a/server/workspace-buehne.js +++ b/server/workspace-buehne.js @@ -58,8 +58,7 @@ import { join } from "node:path"; import { existsSync } from "node:fs"; import { liefereDatei } from "./helfer-ausliefern.mjs"; import { - db, sitzungLesen, einstellung, einstellungSetzen, protokolliere, echteIp, DATEN_ORDNER, -} from "./workspace.js"; + db, sitzungLesen, einstellung, einstellungSetzen, protokolliere, echteIp, DATEN_ORDNER } from "./workspace.js"; /* Der Stand der Einblendung kommt aus derselben Quelle wie im Saal. Er stand hier einmal abgeschrieben -- siehe die Begruendung dort. */ diff --git a/server/workspace-manager-ziele.js b/server/workspace-manager-ziele.js index 64dfe264..c6aedab5 100644 --- a/server/workspace-manager-ziele.js +++ b/server/workspace-manager-ziele.js @@ -53,8 +53,7 @@ import express from "express"; import { - db, sitzungLesen, protokolliere, echteIp, siehtAlles, ROLLEN_NAME, -} from "./workspace.js"; + db, sitzungLesen, protokolliere, echteIp, siehtAlles, ROLLEN_NAME } from "./workspace.js"; import { AUFGABEN, AUFGABEN_SCHLUESSEL, PFLICHT_ROLLEN, ERSTER_MONAT, FREIGABE_SEKUNDEN, diff --git a/server/workspace-material.js b/server/workspace-material.js index 1227e92a..38f26e2d 100644 --- a/server/workspace-material.js +++ b/server/workspace-material.js @@ -61,8 +61,7 @@ import { liefereDatei } from "./helfer-ausliefern.mjs"; import { randomBytes } from "node:crypto"; import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, - istDogFather, TEAM_DOGI_ROLLEN, TREFF_ROLLEN, -} from "./workspace.js"; + istDogFather, TEAM_DOGI_ROLLEN, TREFF_ROLLEN } from "./workspace.js"; /* DIE UHR DES HAUSES, nicht `new Date().toISOString().slice(0,10)`. Das waere UTC -- zwischen Mitternacht und zwei Uhr laege der Tag damit einen zurueck, und ein Stueck, das heute anfaengt, staende diff --git a/server/workspace-reports.js b/server/workspace-reports.js index 90cd3c1c..d6287557 100644 --- a/server/workspace-reports.js +++ b/server/workspace-reports.js @@ -21,8 +21,7 @@ import express from "express"; import { db, protokolliere, echteIp, sitzungLesen, betreuteIds, darfCreator, istLeitung, istDogFather, siehtAlles, sichtbareCreatorIds, - heuteLokal, tagLokal, -} from "./workspace.js"; + heuteLokal, tagLokal } from "./workspace.js"; import { verteilen } from "./workspace-womit.js"; import { BEREICHE } from "./workspace-bereiche.js"; diff --git a/server/workspace-video.js b/server/workspace-video.js index 0dda8119..e5f668d0 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -48,8 +48,7 @@ import { db, protokolliere, echteIp, sitzungLesen, DATEN_ORDNER, KANAELE, kanalVonHandle, TREFF_BRETTER, brettAufDieserAdresse, heuteLokal, - hausFuerNeuenEintrag, -} from "./workspace.js"; + hausFuerNeuenEintrag } from "./workspace.js"; import { darfSchreiben, sofortFreigeben } from "./workspace-treff.js"; import { sichtbar } from "./workspace-bereiche.js"; import { bildArtVon } from "./workspace-dateien.js"; diff --git a/server/workspace-zuteilung.js b/server/workspace-zuteilung.js index e154c198..6b878d8e 100644 --- a/server/workspace-zuteilung.js +++ b/server/workspace-zuteilung.js @@ -56,8 +56,7 @@ import { db, protokolliere, echteIp, darfAufgabenVerteilen, entscheidetUeberAufgaben, istDogFather, istHand, sitzungLesen, - heuteLokal, -} from "./workspace.js"; + heuteLokal } from "./workspace.js"; import { TEAM_DOGI_ROLLEN } from "./crew-adresse.js"; /* WER ES ERFAEHRT -- an einer Stelle fuer beide Bewerbungswege. Begruendung samt Messung steht in der Datei selbst. */ diff --git a/server/workspace.js b/server/workspace.js index 4e6436e9..fe3bc769 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -6636,6 +6636,57 @@ export function echteIp(req) { return req.ip || "?"; } +/* ===================================================================== + KOMMT DIE ANFRAGE VON UNSERER EIGENEN SEITE? (07.10.2026) + ===================================================================== + + Der Schutz gegen CSRF: Ein Browser schickt bei jedem schreibenden + Aufruf mit, von welcher Seite er kommt. Stimmt die nicht mit unserer + ueberein, hat jemand anderes das Formular abgeschickt -- mit dem + Keks des Angemeldeten, aber nicht mit seinem Willen. + + WARUM DIESE FUNKTION JETZT HIER STEHT UND NICHT 35-MAL DANEBEN: + + Am 07.10.2026 nachgezaehlt: 35 Dateien fuehrten je eine eigene + Abschrift -- in DREI verschiedenen Formulierungen. Keine davon war + gefaehrlich falsch, aber drei Fassungen einer Sicherheitsregel sind + drei Gelegenheiten, sie einmal falsch zu aendern. Und zehn Router + mit schreibenden Wegen hatten gar keine: anleitung, befinden, + buehne, hilfe, manager-ziele, material, reports, support, video, + zuteilung. + + `pruef-alle-wege` hatte davon VIER gemeldet -- nicht weil die + anderen sicher waren, sondern weil sie auf die Probe-Nutzlast + zufaellig 400 oder 404 antworten statt 200. Eine Luecke, die nur + deshalb nicht auffaellt, weil der Angriff gerade schlecht geraten + ist, ist eine Luecke. + + OHNE HERKUNFT GEHT ES DURCH, und das ist Absicht: Server-zu-Server, + Kalender-Abrufe und andere Nicht-Browser schicken keinen + Origin-Kopf. Ein Browser schickt ihn bei schreibenden Aufrufen + immer -- genau dort greift die Regel. + + LESENDE AUFRUFE GEHEN EBENFALLS DURCH. CSRF ist ein Problem der + WIRKUNG: Was nichts aendert, kann auch fremd ausgeloest nichts + anrichten. Diese Zeile ist noetig, damit die Schranke an einem + ganzen Router haengen darf (`use`) statt an jedem schreibenden Weg + einzeln -- und „an jedem einzeln" ist genau die Bauweise, bei der + der naechste neue Weg vergessen wird. `workspace-spenden.js` macht + es seit jeher schon so. + + WAS DAS NICHT ENTSCHULDIGT: Ein GET, der etwas aendert, waere ein + eigener Fehler -- und gegen den hilft keine Herkunftspruefung, + sondern nur, ihn nicht zu bauen. */ +export function gleicheHerkunft(req, res, next) { + if (req.method === "GET" || req.method === "HEAD") return next(); + const herkunft = req.get("origin"); + if (!herkunft) return next(); + let erlaubt; + try { erlaubt = new URL(herkunft).host === req.get("host"); } catch { erlaubt = false; } + if (!erlaubt) return res.status(403).json({ fehler: "fremde_herkunft" }); + next(); +} + function hashe(code, salt, N) { return scryptSync(code, salt, SCRYPT.keylen, { N, r: SCRYPT.r, p: SCRYPT.p, maxmem: 256 * 1024 * 1024 }).toString("hex");