/workspace/profil.html -- Stammdaten, Ziele und 90-Tage-Plan, genau nach
Seite 5 des Konzepts. Management waehlt oben den Creator aus, ein Creator
sieht nur sein eigenes Profil.
Sicherheitskern ist das Feld admin_notiz. Das Konzept fordert "private
Admin-Notizen separat". Die Notiz wird deshalb nicht im Browser
ausgeblendet, sondern gar nicht erst gesendet: Die Spaltenliste der
Abfrage haengt an der Rolle (FELDER_OFFEN / FELDER_ADMIN). Dasselbe gilt
fuer Plan-Start und Review-Termin.
Geprueft:
- In der kompletten Rohantwort an den Creator kommt der Inhalt der
internen Notiz 0-mal vor
- Creator auf fremdes Profil: 404 (lesend wie schreibend)
- Scout auf ein Profil: 404, profil.html leitet ihn weg
- Creator setzt admin_notiz selbst: wird stillschweigend ignoriert,
der Inhalt bleibt unveraendert
- Profil einer Nicht-Creator-Person: 404
Dabei ist ein aelterer Fehler aufgefallen: /api/ich lieferte nur Name und
Rolle, nicht die eigene Nummer. Dadurch rief die Profilseite eines
Creators /api/profil/undefined auf und blieb leer. Derselbe Fehler machte
in der Personenverwaltung den Selbstvergleich unwirksam -- beim eigenen
Eintrag erschien ein "Sperren"-Knopf, den der Server dann ablehnte.
/api/ich liefert jetzt zusaetzlich die id.
Im Protokoll landen nur die Feldnamen, nie die Inhalte: Im Profil stehen
persoenliche Angaben, die nicht zusaetzlich im Audit-Log auftauchen
sollen.
Aufgaben:
- Bearbeiten-Dialog (Titel, Beschreibung, Prioritaet, Frist, Zuordnung).
Als natives <dialog>: Fokusfang, Esc zum Schliessen und Abdunklung
ohne eigenen Code.
- Loeschen nur fuer Management, mit Rueckfrage und Protokolleintrag.
Das Konzept will, dass Erledigtes stehen bleibt -- Loeschen ist der
Ausnahmefall fuer Fehleintraege, nicht der normale Abschluss.
Personen (/workspace/personen.html, nur Management):
- Anlegen, Code erneuern, sperren/entsperren, Protokollansicht
- Der Code wird genau einmal in der Antwort zurueckgegeben, nie
gespeichert; beim Schliessen auch aus dem Dokument entfernt
- Selbstschutz: niemand kann sich selbst sperren, und das letzte aktive
Management laesst sich nicht sperren -- sonst kaeme niemand mehr hinein
- Code fuer sich selbst tauschen nur mit ausdruecklicher Bestaetigung,
weil es die eigene Sitzung sofort beendet
Zwei Fehler, die beim Testen aufgefallen sind:
1. Rollenpruefung fehlte beim Ausliefern der Seiten. Ein Creator bekam
personen.html mit HTTP 200 -- die Schnittstellen wiesen ihn zwar ab,
das Geruest der Seite war aber sichtbar. GESCHUETZT ist jetzt eine
Zuordnung Pfad -> erlaubte Rollen statt einer blossen Liste.
2. Das Protokoll nannte den falschen Verursacher. personAnlegen trug die
NEU ANGELEGTE Person als person_id ein, der Eintrag las sich also so,
als haette sie sich selbst angelegt. Akteur und Betroffener sind jetzt
getrennt: Akteur in person_id, Betroffener im Text. Ueber die
Kommandozeile angelegte Personen zeigen korrekt keinen Akteur.
Erster echter Arbeitsbereich aus dem Konzept. Kanban mit offen / in
Arbeit / Review / erledigt, dazu Prioritaet, Frist, Verantwortlicher und
Zuordnung zu einem Creator-Bereich.
Kern ist die Datentrennung, und die sitzt AUSSCHLIESSLICH im Server --
in jeder einzelnen Abfrage, an einer Stelle definiert (sichtbar()):
admin sieht alles
creator sieht seinen Bereich und was ihm zugewiesen ist
scout sieht nur, was ihm zugewiesen ist
Geprueft mit vier Testkonten:
- Chef sieht 4, Luna 2, Mika 1, Sam 1 Aufgaben
- Luna auf Mikas Aufgabe: 404 (nicht 403 -- sonst liesse sich durch
Ausprobieren herausfinden, welche Nummern es gibt)
- Luna legt Aufgabe mit creator_id=Mika an: wird still auf ihren eigenen
Bereich umgebogen, Mika sieht sie nicht
- Anfrage mit fremdem Origin: 403
- ohne Anmeldung: 401, ungueltiger Status/leerer Titel: 400
- Scout bekommt in der Personenliste nur sich selbst
- Dashboard-Zahlen je Rolle korrekt eingegrenzt
Weitere Punkte:
- Texte werden im Browser nur ueber textContent gesetzt, nie innerHTML --
ein Aufgabentitel darf keine Auszeichnung einschleusen
- ueberfaellig = Frist vorbei UND nicht erledigt; die Kachel faerbt sich
nur, wenn wirklich etwas ansteht
- erledigt_am wird gesetzt bzw. wieder geleert, wenn eine Aufgabe
zurueckgeholt wird
- Erledigtes verschwindet nicht, wie im Konzept gefordert
Erster funktionierender Login fuer /workspace. Bewusst OHNE neue
Abhaengigkeiten: Node 24 bringt node:sqlite mit, Hashing und Zufall
kommen aus node:crypto. Nichts zu kompilieren, keine fremde Lieferkette
an einer Stelle, an der es um Zugangsdaten geht.
Sicherheit:
- Codes liegen nur als scrypt-Hash (N=32768) mit eigenem Salt in der DB
- Vergleich in konstanter Zeit (timingSafeEqual)
- Sitzungstoken 32 Byte Zufall, in der DB nur als SHA-256
- Cookie httpOnly, SameSite=lax, Path=/workspace, secure abhaengig von
req.secure (live immer an, nur der lokale http-Test kommt ohne aus)
- Sperre nach 8 Fehlversuchen je IP fuer 10 Minuten -- danach ist auch
der richtige Code blockiert (geprueft)
- gleiche Fehlermeldung bei falscher Rolle und falschem Code
- Audit-Log fuer Anmeldung, Fehlversuch, Sperre, Abmeldung, Codewechsel
Die Datenbank liegt AUSSERHALB des Repos (../workspace-daten/): sonst
waere sie ueber express.static aus dem Netz erreichbar, und ein git pull
wuerde Nutzdaten anfassen.
workspace.js kann die Website nicht mitreissen: node:sqlite wird erst bei
Bedarf per createRequire geladen, jede Route faengt ihre Fehler selbst ab.
Faellt die DB aus, antwortet nur /workspace/api/* mit 503.
Codes werden ausschliesslich auf der Kommandozeile erzeugt
(server/workspace-code.js) und dort einmal angezeigt -- nie in Git.
Ausserdem: start.html als geschuetzte Seite nach dem Anmelden. Der Schutz
sitzt serverseitig vor express.static, nicht nur im Browser.
Fuenf Schutz-Kopfzeilen waren gesetzt, die wichtigste fehlte. Sie
entscheidet als einzige darueber, ob eingeschleuster Text zu
ausgefuehrtem Code wird oder sichtbarer Text bleibt.
WARUM NICHT DER BEQUEME WEG
Ueblich waere script-src 'self' 'unsafe-inline'. Eine Zeile, nichts geht
kaputt -- und der Schutz ist weg: Der Browser kann nicht unterscheiden,
ob ein Skript im Seitentext vom Entwickler stammt oder von einem
Angreifer. Das Ergebnis ist eine Kopfzeile, die gut aussieht und im
Ernstfall nichts tut.
Der Bestand liess den sauberen Weg zu: keine fremden Skriptquellen,
keine externen Schriften, ein Inline-Block je Seite. Von jedem Block
wird die Pruefsumme gebildet.
DIE PRUEFSUMMEN STEHEN BEWUSST NICHT IM CODE
Das waere hier eine Falle mit Ansage: Statische Dateien gehen per "git
pull" live, OHNE Neustart. Eine fest hinterlegte Summe waere nach der
naechsten Textaenderung falsch -- und die Seite wuerde ihr eigenes
Skript nicht mehr ausfuehren. Sichtbar erst im Browser des Besuchers,
nicht beim Deploy, und aussehend wie kaputtes JavaScript.
Deshalb liest die Middleware die Datei selbst und merkt sich das
Ergebnis, solange die Aenderungszeit gleich bleibt. Ein Test aendert
index.html im laufenden Betrieb und prueft, dass die Summe nachzieht
und die Seite weiterlaeuft.
WAS DER TEST GEFUNDEN HAT
Die erste Fassung haette die Startseite und stimmen.html beschaedigt:
Team-Fotos, Event des Jahres und die Stimmen kommen von der
postfach-Subdomain, img-src erlaubte nur 'self'. Der Deploy haette
Erfolg gemeldet, der Server waere gestartet -- und die Bilder waeren
weg gewesen. Gefunden, weil der Test alle 48 Seiten in einem echten
Browser oeffnet und mitschreibt, was blockiert wird.
Ein zweiter Fehlschlag lag am Test selbst: Er verlangte eine Pruefsumme
auf jeder Seite, auch auf denen ohne Inline-Block. Ein Test, der
Unmoegliches fordert, wird frueher oder spaeter abgeschaltet -- er
unterscheidet jetzt nach dem tatsaechlichen Inhalt der Datei.
DREI onclick-ATTRIBUTE ENTFERNT
Sie haetten 'unsafe-inline' erzwungen. Zweimal ein "Coming soon"-Knopf,
dessen onclick nur Klicks abfing -- ein <a> ohne href tut das von
selbst, ganz ohne Skript. Einmal ein Schliessen-Knopf im
Verwaltungsbereich, jetzt mit angehaengtem Zuhoerer.
GEGENPROBE
Ohne sie waere der Rest wertlos: Eine Richtlinie, die alles erlaubt,
blockiert auch nichts und besteht jede Pruefung. Der Test schleust
deshalb echten Code ein -- ein Inline-Skript und eines von fremder
Adresse -- und beide muessen scheitern.
style-src behaelt 'unsafe-inline': 587 style-Attribute im Bestand, deren
Umbau ein echtes Risiko fuers Aussehen waere, bei kleinem Gewinn. Ueber
Stile laesst sich verschleiern und ueberdecken, aber kein Code
ausfuehren. Bleibt als eigener Punkt auf der Liste.
15 Pruefungen gruen, 48 Seiten sauber, Portfolio-Suite unveraendert 35.
Co-Authored-By: Claude Opus 5 <[email protected]>
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign.
Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js:
gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil
die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man
/webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich
inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden
lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit
ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests.
Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en,
fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau
EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall
widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung
auseinander.
Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst
KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten
weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests.
Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen:
40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im
Designsystem statt einzeln pro Seite.
PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit
intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und
Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent.
Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten
fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server.
Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe
getrennt.
Co-Authored-By: Claude Opus 5 <[email protected]>
Nutzer-Report: sah den Nav-Fix und die neue installierbare Verwaltungsseite
trotz erfolgreichem Deploy nicht. Ursache: Express schickt standardmaessig
KEIN Cache-Control mit -- da die Seite hinter Cloudflare (orange-cloud)
liegt, sprang Cloudflare dafuer mit seinem eigenen Standardwert ein
(Browser Cache TTL 4 Std, per curl bestaetigt: max-age=14400). Ein frischer
Deploy war dadurch bis zu 4 Std lang im eigenen Browser-Cache jedes/jeder
Besuchers unsichtbar. Cloudflare respektiert ein vom Origin gesetztes
Cache-Control -- jetzt explizit "no-cache" gesetzt (erzwingt Revalidierung
per ETag bei jedem Laden, kein Performance-Verlust durch schnelle
304-Antworten bei unveraendertem Inhalt).
Co-Authored-By: Claude Opus 5 <[email protected]>
Sicherheits-Audit vor dem geplanten oeffentlichen Start morgen (21.08.2026,
Nutzer-Anfrage: "duerfen die leute keinen zugriff auf veraenderungen haben").
SITE_DIR ist der GESAMTE Repo-Ordner (join(__dirname, "..")), express.static
lieferte daher nicht nur die Website aus, sondern auch:
- server/ (inkl. gate.js, das komplette Sicherheitskonzept im Klartext)
- server-internal/ (Admin-/Supporter-Backend-Quellcode)
- cloudflare-worker/ (altes Backend)
- .git/ (VOLLSTAENDIGE Commit-Historie, rekonstruierbar per Git-Dump)
- CLAUDE.md, DEPLOY.md, wrangler.toml, netlify.toml, gate-worker.js,
gate.html.bak-07-08-2026 (Alt-Backup-Datei einer frueheren Session)
Live nachgewiesen (mit gueltigem Zugangscode -- morgen faellt die Schranke
fuer ALLE weg): /server/gate.js und /.git/config lieferten HTTP 200.
Ursache: serve-static blockt per Default nur Dateien, deren EIGENER Name
mit einem Punkt beginnt (server/.env -> zufaellig schon 404), aber NICHT
rekursiv -- .git/config wird trotzdem ausgeliefert, weil "config" selbst
nicht mit einem Punkt beginnt, nur der Ordner davor.
Fix: eigene Sperr-Middleware VOR express.static, unabhaengig von
gateMiddleware (bleibt also auch nach dem Entfernen der Zugangsschranke
wirksam). Blockt ganze Ordner (server/, server-internal/,
cloudflare-worker/) + versteckte Ordner/Dateien rekursiv (jedes
Pfadsegment, das mit "." beginnt, ausser .well-known) + eine feste Liste
an Alt-Dateien + jedes *.bak-Muster, damit auch kuenftige Backup-Reste
automatisch mitgeschuetzt sind.
Lokal mit echtem Express-Server verifiziert (gateMiddleware absichtlich
deaktiviert, um exakt den morgigen "oeffentlich"-Zustand zu simulieren):
alle vorher gefundenen Luecken jetzt 404, alle echten Seiten/Assets
(index.html, main.css, main.js, manifest.json, robots.txt, favicon)
weiterhin 200.
Getrennt prooft: server-internal/ (eigener Dienst unter
postfach.dogfather-universe.com, Port 4200) hat sein EIGENES,
unabhaengiges Session-System -- jede /admin/*-Route ist einzeln per
requireTeamSession-Middleware abgesichert (in index.js durchgezaehlt,
keine Ausnahme gefunden), live mit einer unauthentifizierten Anfrage
gegen /admin/users/list bestaetigt (401). Dieser Dienst war nie vom
Website-Gate abhaengig und ist von diesem Fund nicht betroffen.
Co-Authored-By: Claude Opus 5 <[email protected]>
- Login-Sperre war umgehbar: clientKey() vertraute dem Header CF-Connecting-IP.
Bei Cloudflare war das sicher (CF ueberschreibt ihn), auf dem eigenen Server nicht:
der Ursprungsserver ist auch direkt unter seiner IP erreichbar, dort konnte der
Header frei gesetzt und die 5-Versuche-Sperre komplett ausgehebelt werden
(nachgewiesen). Jetzt req.ip hinter trust proxy.
- Absturzsicherheit: Express 4 faengt Fehler aus async-Handlern nicht ab, eine
einzige fehlerhafte Anfrage konnte den ganzen Dienst beenden. wrap() um alle
Handler, zentraler Fehler-Handler, unhandledRejection/uncaughtException-Netz.
- Sicherheits-Header (X-Content-Type-Options, X-Frame-Options, Referrer-Policy,
Permissions-Policy, HSTS) wurden bisher nur ueber die Datei _headers gesetzt,
die auf dem eigenen Server wirkungslos ist. Jetzt im Express-Server.
- x-powered-by abgeschaltet.
- Datenschutzerklaerung/AGB: nannten Cloudflare als Hoster und eine Cloudflare-D1-
Datenbank. Jetzt korrekt netcup (Rechenzentrum Nuernberg) als Hoster, Cloudflare
als vorgeschaltetes CDN mit Drittlandhinweis.