Vier stillgelegte Pruefungen, zwei kaputte Zeichen -- und die Wache dagegen

Gefunden beim Vorbereiten der KLIPY-Anbindung, nicht gesucht.

DER VERLORENE BACKSLASH
In fuenf Dateien stand in einem Suchmuster das BYTE 0x08 statt der
zwei Zeichen \ und b. Gemeint war die Wortgrenze; 0x08 ist das
Rueckschritt-Zeichen und kommt in keinem Text vor. Gemessen:

  /<0x08>modis?<0x08>/i.test("Die Modis sind da")  ->  false
  /\bmodis?\b/i        .test("Die Modis sind da")  ->  true

Folgen, je Stelle:

  pruef-nachwuchs         Das Muster stand in einer VERNEINUNG. Die
                          Zeile lautete damit ok(!false) -- dauerhaft
                          gruen, ohne etwas zu messen. Ausgerechnet
                          die Zeile, die das Durchsickern des
                          Rollennamens verhindern soll.
  pruef-personen-formular dasselbe Muster, dieselbe Verneinung
  pruef-nachfrage         hatDialogMitFeld war immer falsch. Seiten
                          mit einem Formular-Dialog galten als "totes
                          Gewicht" -- der Kommentar direkt darueber
                          warnt woertlich vor genau diesem Schaden.
  pruef-entwicklung       zwei von drei Alternativen tot
  workspace-treff.js      nur ein Kommentar, aber unlesbar

ZWEI KAPUTTE ZEICHEN, BEIDE SICHTBAR
  content: "<0x15>C9<0x00>A0"  in chat.css. Gemeint war U+25C9 und
  ein geschuetztes Leerzeichen. Live stand woertlich "C9 A0" vor
  jedem hervorgehobenen @rudel -- am echten Server nachgemessen.

  content: "¹3"  in chat.css UND entwicklung.css. Gemeint war ein
  Haken. Auf der gewaehlten Kachel stand "¹3"; zu sehen in Filipes
  Bildschirmfoto.

Repariert wird mit der Escape-Schreibweise ("\2713"), nicht mit dem
Zeichen selbst: Sie besteht nur aus ASCII und ueberlebt jede
Kodierung.

WAS DIESE FEHLERART BESONDERS MACHT
Niemand sieht sie beim Lesen. Der erste war committet, ausgeliefert
und lag live -- und als ich in der Datei danach suchte, meldete grep
"Binary file matches" und zeigte die Zeile gar nicht erst an. Wer den
Unterschied durchsieht, liest content: "C9 A0" und denkt sich nichts.

Deshalb neu: server/pruef-zeichen.mjs (7 Pruefungen). Sie sucht rohe
Steuerzeichen in allen ausgelieferten und allen Serverdateien, dazu
Ersatzzeichen und Kodierungstruemmer in CSS-content-Angaben. Mit
Gegenprobe in beide Richtungen -- sie weist an einer selbst gebauten
Datei nach, dass sie anschlaegt, UND dass sie bei einer sauberen
schweigt.

Die erste Fassung der Regel war zu weit: Sie meldete acht KORREKTE
Zeichen (✓, ↯, ↻, „, ⚠). Eine Warnung, die immer kommt, ist keine
Warnung mehr. Die engere Regel kommt aus dem Befund selbst -- der
Latin-1-Block U+0080..U+00BF, in dem echte Typografie nie steht.
Nachgemessen ueber alle 203 content-Angaben im Haus: keine einzige
benutzt ihn.

GEHEIMNISSE STEHEN NICHT MEHR IM PROTOKOLL
einstellungSetzen() schrieb immer Name UND Wert. Nachgemessen in der
echten Datenbank: kopie_schluessel steht vollstaendig drin,
vapid_paar wurde bei 120 Zeichen abgeschnitten -- kurz VOR dem
privaten Teil. Dass der heil blieb, lag an einer Laengengrenze, nicht
an einer Absicht. Keine offene Tuer (das Protokoll liegt hinter
nurAdmin), aber der Wert wandert in jede Sicherung.

Es ist eine REGEL und keine Liste: Wer eine Einstellung so benennt,
meint ein Geheimnis. Stehen bleibt, DASS sich etwas geaendert hat,
wann und durch wen -- nur der Wert fehlt.

EINE VERALTETE PRUEFUNG, UMGEDREHT STATT GELOESCHT
pruef-nachwuchs verlangte, dass die rechte Hand keine Zugaenge
anlegen kann. Das war bis zum 22.09. richtig; dann hat Filipe es
umgedreht ("damit sie das auch machen kann wenn er live ist"). Die
Pruefung war seither rot, ohne dass es auffiel -- sie lief nicht.
Jetzt prueft sie beides: dass die rechte Hand es darf UND dass die
Grenzen stehen (sperren 404, Protokoll 404). Eine Erlaubnis ohne
Grenze ist keine Entscheidung, sondern ein Loch.

Und tools/nachtlauf.mjs wird nachgetragen -- bisher unversioniert.

Zahlen: pruef-nachwuchs 260, pruef-entwicklung 48,
pruef-personen-formular 43, pruef-zeichen 7 (neu), pruef-ports 8.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-23 14:29:32 +02:00
co-authored by Claude Opus 5
parent c0d2896419
commit a9d920e7d0
10 changed files with 683 additions and 10 deletions
+1 -1
View File
@@ -327,7 +327,7 @@ console.log("=== Die Kacheln ===");
Untertitel. Wer morgen eine Kachel "Dein Kopf" nennt und dasselbe
verspricht, ist damit mitgeprueft; die Namensliste haette ihn
durchgelassen. */
const verspricht = (k) => /nur du|nur dich|siehst nur du/i.test(k.unter || "");
const verspricht = (k) => /\bnur du\b|\bnur dich\b|siehst nur du/i.test(k.unter || "");
const heikel = alleX.filter((k) => verspricht(k) && k.ziel === "entwicklung.html");
ok(alleX.some(verspricht) && heikel.length === 0,
`${wer}: keine Kachel verspricht Vertraulichkeit und fuehrt doch auf die`
Binary file not shown.
+1 -1
View File
@@ -159,7 +159,7 @@ for (const s of seiten) {
einem reinen Formular-Dialog hier als „totes Gewicht" gemeldet --
und jemand naehme die Datei heraus, die dort gerade arbeitet. */
const hatDialogMitFeld = [...html.matchAll(/<dialog[^>]*>[\s\S]*?<\/dialog>/g)]
.some((m) => /<(input|textarea|select)/.test(m[0]));
.some((m) => /<(input|textarea|select)\b/.test(m[0]));
const kannFragen = geladen.some((g) => nutzer.has(g)) || hatDialogMitFeld;
const hatDatei = html.includes("assets/js/nachfrage.js");
if (kannFragen) { mitNutzung++; fragendeSeiten.push(s); if (!hatDatei) ohneDatei.push(s); }
+38 -4
View File
@@ -614,13 +614,47 @@ melde("=== 11. Probe → Zugang ===");
ok(!/personAnlegen|INSERT INTO personen/.test(eigen),
"und der Talente-Server legt selbst keinen einzigen Zugang an");
/* DIE RECHTE HAND SIEHT DEN TRICHTER, VERGIBT ABER KEINE ZUGAENGE. */
/* DIE RECHTE HAND LEGT ZUGAENGE AN -- SEIT DEM 22.09.2026.
Hier stand bis zum 23.09. das Gegenteil: "vergibt aber keine
Zugaenge", geprueft mit 404. Das war bis zum 22.09. richtig. Dann
hat Filipe es ausdruecklich umgedreht -- "damit sie das auch
machen kann wenn er live ist" -- und die Route in
workspace-personen.js oeffnet seither genau zwei Wege: anlegen
und einen Code neu erzeugen.
DIE PRUEFUNG WAR ALSO SEIT EINEM TAG ROT, ohne dass es jemandem
aufgefallen waere -- sie lief in der Zwischenzeit nicht. Das ist
der Normalfall und nicht die Ausnahme: Man startet die
Pruefungen, an denen man gerade baut.
UMGEDREHT WIRD SIE, NICHT GELOESCHT. Eine Erlaubnis ohne Grenze
ist keine Entscheidung, sondern ein Loch. Geprueft wird deshalb
ab jetzt beides: dass die rechte Hand es DARF -- und dass die
Grenzen daneben stehen bleiben. */
const hand = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kHand },
{ name: "Heimlich", rolle: "modi" });
ok(hand.code === 404, `die rechte Hand kommt am Anlegen nicht vorbei (${hand.code})`);
ok(hand.code === 201, `die rechte Hand darf einen Zugang anlegen (${hand.code})`);
/* ABER NICHT ALLES. Sperren gehoert nicht dazu -- das ist die
Grenze, die "anlegen" von "verwalten" trennt. Ohne diese Zeile
waere die Aenderung oben eine Erlaubnis ohne Rand. */
const handSperrt = await roh(`/workspace/api/verwaltung/personen/${json(hand).id}/aktiv`,
CREW, { cookie: kHand }, { aktiv: 0 }, "PUT");
ok(handSperrt.code === 404 || handSperrt.code === 403,
`aber sperren darf sie niemanden (${handSperrt.code})`);
/* UND DAS PROTOKOLL BLEIBT BEI DOGFATHER. Dort stehen die
Einstellungsaenderungen des ganzen Hauses. */
const handProtokoll = await roh("/workspace/api/verwaltung/protokoll", CREW, { cookie: kHand });
ok(handProtokoll.code === 404 || handProtokoll.code === 403,
`und ins Protokoll sieht sie nicht (${handProtokoll.code})`);
/* EIN MODI DARF WEITERHIN GAR NICHTS DAVON. Das ist die eigentliche
Aussage dieses Blocks -- und sie ist unveraendert. */
const einModi = await roh("/workspace/api/verwaltung/personen", CREW, { cookie: kModi },
{ name: "Heimlich", rolle: "modi" });
ok(einModi.code === 404, `ein Modi erst recht nicht (${einModi.code})`);
ok(einModi.code === 404, `ein Modi kommt am Anlegen nicht vorbei (${einModi.code})`);
/* GEGENPROBE: DogFather kann es -- sonst waere "404" oben auch dann
gruen, wenn der Weg gar nicht existiert. */
@@ -1477,7 +1511,7 @@ melde("=== 17. Der Anschluss im Browser ===");
const neueTexte = await seite.evaluate(() =>
[...document.querySelectorAll(".t-start, .t-person")]
.map((e) => e.textContent || "").join(" "));
ok(!/modis?/i.test(neueTexte),
ok(!/\bmodis?\b/i.test(neueTexte),
`die neuen Bausteine nennen die Rolle nicht (${neueTexte.length} Zeichen geprueft)`);
/* GEGENPROBE zur Voraussetzung: Es wurde ueberhaupt etwas
angesehen. Ohne Text waere die Zeile darueber auch dann gruen,
+1 -1
View File
@@ -235,7 +235,7 @@ ok(mass.kartenZeichen.modi && mass.kartenZeichen.modi !== mass.kartenZeichen.adm
Auf dem Bildschirmfoto stand "Sichtbar nur fuer die DogFather-Rolle"
-- in einem Text, den jemand liest, sieht das aus wie ein Fehler,
weil es einer ist. */
ok(mass.kartenTexte.modi && !/(fuer|ueber|waehrend|koennen|moeglich)/
ok(mass.kartenTexte.modi && !/\b(fuer|ueber|waehrend|koennen|moeglich)\b/
.test(mass.kartenTexte.modi),
`und ihre Beschreibung hat echte Umlaute ("${mass.kartenTexte.modi.slice(0, 60)}…")`);
ok(mass.wahlUnten <= mass.formUnten,
+274
View File
@@ -0,0 +1,274 @@
/* =====================================================================
STEHT IN EINER AUSGELIEFERTEN DATEI EIN ZERSTOERTES ZEICHEN?
WARUM ES DIESE PRUEFUNG GIBT: Weil genau das zweimal live stand,
und zwar am selben Tag (23.09.2026), in zwei Dateien.
1. `content: "<0x15>C9<0x00>A0"` in chat.css. Gemeint war U+25C9
(dasselbe Zeichen, das chat.js fuer das Rudel benutzt) und ein
geschuetztes Leerzeichen. Im Quelltext standen stattdessen ZWEI
ROHE STEUERZEICHEN. Auf dem Bildschirm las man deshalb woertlich
"C9 A0" vor jedem hervorgehobenen @rudel -- die Steuerzeichen
selbst sind unsichtbar.
2. `content: "¹3"` in chat.css UND entwicklung.css. Gemeint
war ein Haken. Auf der gewaehlten Kachel stand "¹3".
DAS BESONDERE AN DIESER FEHLERART: Niemand sieht sie beim Lesen.
Punkt 1 war sogar committet, ausgeliefert und lag live -- und als
ich in der Datei danach suchte, meldete grep "Binary file matches"
und zeigte die Zeile gar nicht erst an. Ein Mensch, der den
Unterschied durchsieht, liest `content: "C9 A0"` und denkt sich
nichts dabei.
Gefunden hat es am Ende Filipes Bildschirmfoto, nicht der Quelltext.
Das ist zu spaet und zu zufaellig. Also eine Pruefung.
---------------------------------------------------------------------
WAS GEPRUEFT WIRD, UND WARUM DIE GRENZE DA LIEGT
AUSGELIEFERTE DATEIEN (workspace/, assets/, *.html) -- dort ist
beides ein Fehler: ein Steuerzeichen UND ein Ersatzzeichen (U+FFFD).
Was dort steht, sieht ein Mensch.
SERVERDATEIEN -- dort nur Steuerzeichen. Ein U+FFFD kann in einer
Pruefung absichtlich stehen: pruef-ics.mjs sucht genau danach in
Kalenderdateien. Eine Ausnahmeliste mit Dateinamen waere die
naheliegende Loesung und die schlechtere -- sie altert. Die Grenze
"wird es ausgeliefert?" altert nicht.
TABULATOR, ZEILENVORSCHUB UND WAGENRUECKLAUF sind erlaubt. Alles
andere unter 0x20 ist in Quelltext nie Absicht.
===================================================================== */
import { readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
import { join, dirname, extname, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
const HIER = dirname(fileURLToPath(import.meta.url));
const WURZEL = join(HIER, "..");
let fehler = 0;
let geprueft = 0;
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* NOTBREMSE. Auch eine Pruefung ohne Netzzugriff bekommt eine -- ein
Lauf, der haengt, ist schlimmer als einer, der scheitert (06.09.2026,
damals drei Stunden). */
const notbremse = setTimeout(() => {
console.log(" FEHL Notbremse: die Pruefung haengt.");
process.exit(1);
}, 60_000);
const ENDUNGEN = new Set([".js", ".mjs", ".html", ".css", ".json", ".svg",
".txt", ".webmanifest"]);
/* Ordner, die uns nicht gehoeren oder erzeugt sind. */
const AUSSEN = new Set([".git", "node_modules", "sicherungen", ".tmp"]);
/** Alle Dateien unterhalb eines Ordners, die uns angehen. */
function dateien(ordner) {
const raus = [];
const gehe = (o) => {
let eintraege;
try { eintraege = readdirSync(o, { withFileTypes: true }); } catch { return; }
for (const e of eintraege) {
if (e.name.startsWith(".tmp-")) continue;
const p = join(o, e.name);
if (e.isDirectory()) { if (!AUSSEN.has(e.name)) gehe(p); continue; }
if (ENDUNGEN.has(extname(e.name).toLowerCase())) raus.push(p);
}
};
gehe(ordner);
return raus;
}
/* ---------------------------------------------------------------------
Die beiden Sucher. Getrennt, damit die Gegenprobe jeden einzeln
nachweisen kann.
--------------------------------------------------------------------- */
/** Rohe Steuerzeichen -- alles unter 0x20 ausser \t \n \r. */
function steuerzeichenIn(roh) {
const treffer = [];
for (let i = 0; i < roh.length; i++) {
const b = roh[i];
if (b >= 0x20 || b === 0x09 || b === 0x0a || b === 0x0d) continue;
/* Die Zeile mitgeben -- eine Bytenummer allein sucht man lange. */
const zeile = roh.subarray(0, i).toString("utf8").split("\n").length;
treffer.push({ zeile, byte: b });
}
return treffer;
}
/** Ersatzzeichen U+FFFD -- in UTF-8 die drei Bytes ef bf bd. */
function ersatzzeichenIn(roh) {
const treffer = [];
let i = -1;
while ((i = roh.indexOf(Buffer.from([0xef, 0xbf, 0xbd]), i + 1)) !== -1) {
treffer.push({ zeile: roh.subarray(0, i).toString("utf8").split("\n").length });
}
return treffer;
}
/* =====================================================================
1. GEGENPROBE ZUERST
Eine Pruefung, die immer bestaetigt, bestaetigt nichts. Bevor sie
ueber echte Dateien redet, weist sie nach, dass sie ueberhaupt
anschlagen KANN -- an einer Datei, die sie sich selbst baut. Ohne
das waere ein gruener Lauf hier auch dann gruen, wenn beide Sucher
ins Leere greifen (der Fehler vom 01.09.2026, zweimal an einem Tag).
===================================================================== */
console.log("=== 0. Kann die Pruefung ueberhaupt Alarm schlagen? ===");
{
const probe = join(tmpdir(), "pruef-zeichen-probe.css");
try {
/* Genau die beiden Faelle, die live standen. */
writeFileSync(probe, Buffer.concat([
Buffer.from('a { content: "'),
Buffer.from([0x15]), Buffer.from("C9"),
Buffer.from([0x00]), Buffer.from('A0"; }\n'),
Buffer.from('b::after { content: "'), Buffer.from([0xef, 0xbf, 0xbd]), Buffer.from('"; }\n'),
]));
const roh = readFileSync(probe);
const st = steuerzeichenIn(roh);
const er = ersatzzeichenIn(roh);
ok(st.length === 2, `sie findet rohe Steuerzeichen (${st.length}, erwartet 2)`);
ok(er.length === 1, `sie findet Ersatzzeichen (${er.length}, erwartet 1)`);
/* Und die andere Richtung: saubere Datei, kein Alarm. Eine
Pruefung, die das richtige Verhalten bestraft, wird abgeschafft
(03.09.2026). */
writeFileSync(probe, 'a { content: "\\25C9\\A0"; }\n\tb { color: red; }\r\n');
const sauber = readFileSync(probe);
ok(steuerzeichenIn(sauber).length === 0 && ersatzzeichenIn(sauber).length === 0,
"und sie schweigt bei einer sauberen Datei -- auch mit Tabulator und CRLF");
} finally {
try { unlinkSync(probe); } catch { /* beim Aufraeumen ist Scheitern egal */ }
}
}
/* =====================================================================
2. DIE AUSGELIEFERTEN DATEIEN
===================================================================== */
console.log("");
console.log("=== 1. Was der Browser bekommt ===");
const geliefert = [
...dateien(join(WURZEL, "workspace")),
...dateien(join(WURZEL, "assets")),
...dateien(WURZEL).filter((p) => dirname(p) === WURZEL && extname(p) === ".html"),
];
{
const schlimm = [];
for (const p of geliefert) {
const roh = readFileSync(p);
for (const t of steuerzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
}
for (const t of ersatzzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Ersatzzeichen)`);
}
}
/* DIE ANZAHL STEHT IN DER BEDINGUNG, nicht nur im Text. Waere der
Ordner leer oder der Pfad falsch, meldete "keine Fundstellen"
sonst Erfolg -- gruen, ohne eine einzige Datei angesehen zu
haben. */
ok(geliefert.length > 50 && schlimm.length === 0,
`${geliefert.length} ausgelieferte Dateien, ${schlimm.length} mit zerstoerten Zeichen`);
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
if (schlimm.length > 12) console.log(` ... und ${schlimm.length - 12} weitere`);
}
/* =====================================================================
3. DIE SERVERDATEIEN -- nur Steuerzeichen
===================================================================== */
console.log("");
console.log("=== 2. Der Server ===");
{
const schlimm = [];
const serverDateien = [...dateien(join(WURZEL, "server")), ...dateien(join(WURZEL, "tools"))];
for (const p of serverDateien) {
const roh = readFileSync(p);
for (const t of steuerzeichenIn(roh)) {
schlimm.push(`${relative(WURZEL, p)}:${t.zeile} (Byte 0x${t.byte.toString(16).padStart(2, "0")})`);
}
}
ok(serverDateien.length > 50 && schlimm.length === 0,
`${serverDateien.length} Serverdateien, ${schlimm.length} mit Steuerzeichen`);
for (const s of schlimm.slice(0, 12)) console.log(" " + s);
}
/* =====================================================================
4. DIE STELLE, AN DER ES ZWEIMAL PASSIERT IST
`content:` in CSS ist der eine Ort, an dem ein Zeichen SICHTBAR
wird, ohne dass es im Text steht. Beide Fehler sassen dort.
---------------------------------------------------------------------
DIE REGEL WAR IM ERSTEN ANLAUF ZU WEIT -- und das ist hier notiert,
weil es der wichtigere Teil ist.
Zuerst stand hier "jedes Zeichen ueber 0x7f ist verdaechtig, schreib
es als Escape". Das meldete auf Anhieb ACHT Stellen: ✓ in vier
Dateien, ↯, ↻, „ und ⚠. Alle acht sind richtig, absichtlich und
seit Monaten in Ordnung. Eine Pruefung, die beim ersten Lauf acht
korrekte Stellen anmeckert, wird beim neunten Mal weggeklickt -- und
dann faengt sie auch den echten Fall nicht mehr (03.09.2026).
DIE ENGERE REGEL KOMMT AUS DEM BEFUND SELBST. Das kaputte Zeichen
war U+00B9 (hochgestellte Eins) -- ein Rest aus dem Latin-1-Block.
Genau dort, zwischen U+0080 und U+00BF, liegen die typischen
Truemmer einer verungluekten Kodierung: ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ®
° ± ² ³ ´ µ ¶ · ¸ ¹ º » ¼ ½ ¾.
Nachgemessen ueber alle 203 content-Angaben im Haus: KEINE
einzige benutzt ein Zeichen aus diesem Block. Alle echten liegen
darueber (✓ = U+2713, ⚠ = U+26A0, „ = U+201E). Die Regel ist damit
heute gruen, haette den Fehler aber gefunden -- und sie ist nicht
geraten, sondern an den vorhandenen Daten geprueft.
===================================================================== */
console.log("");
console.log("=== 3. Zeichen, die CSS auf den Bildschirm schreibt ===");
/** Truemmerverdacht: Latin-1-Block, wo echte Typografie nie hingehoert. */
const istTruemmer = (c) => c >= 0x80 && c <= 0xbf;
{
const verdaechtig = [];
let stellen = 0;
for (const p of geliefert.filter((x) => extname(x) === ".css")) {
const text = readFileSync(p, "utf8");
for (const m of text.matchAll(/content:\s*"([^"]*)"/g)) {
stellen++;
for (const z of m[1]) {
const c = z.codePointAt(0);
if (istTruemmer(c)) {
verdaechtig.push(`${relative(WURZEL, p)}: content "${m[1]}" enthaelt U+${c.toString(16).toUpperCase().padStart(4, "0")}`);
}
}
}
}
ok(stellen > 50 && verdaechtig.length === 0,
`${stellen} content-Angaben in CSS, ${verdaechtig.length} mit Kodierungstruemmern`);
for (const s of verdaechtig.slice(0, 12)) console.log(" " + s);
/* GEGENPROBE: Wuerde sie den echten Fall sehen? Nicht theoretisch --
an genau der Zeichenfolge, die in chat.css stand. */
const kaputt = [...'content: "¹3"'.matchAll(/content:\s*"([^"]*)"/g)]
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
const heil = [...'content: "✓"'.matchAll(/content:\s*"([^"]*)"/g)]
.flatMap((m) => [...m[1]]).some((z) => istTruemmer(z.codePointAt(0)));
ok(kaputt && !heil,
'sie erkennt das echte "¹3" und laesst den richtigen Haken in Ruhe');
}
clearTimeout(notbremse);
console.log("");
console.log(fehler === 0
? `Alles in Ordnung — ${geprueft} Prüfungen`
: `${fehler} FEHLER — ${geprueft} Prüfungen`);
process.exit(fehler === 0 ? 0 : 1);
+1 -1
View File
@@ -452,7 +452,7 @@ treffRouter.get("/workspace/api/treff/regeln", (req, res) => {
Skript. Jetzt kommt er von hier.
UND DER SCHLUESSEL HIESS SELBST SO: `modi_frist_tage`. Das
Wortleck-Muster ist `modi` und trifft das nicht, weil danach
Wortleck-Muster ist `\bmodi\b` und trifft das nicht, weil danach
ein Unterstrich folgt -- ein Leck durch eine Luecke im Muster,
nicht durch eine Entscheidung. Deshalb heisst das Feld jetzt
nach dem, was es bedeutet. */
+39 -1
View File
@@ -7220,15 +7220,53 @@ export function einstellung(schluessel, vorgabe = null) {
}
}
/* WELCHE EINSTELLUNGEN NICHT IM KLARTEXT INS PROTOKOLL GEHOEREN
---------------------------------------------------------------------
Gefunden am 23.09.2026, beim Vorbereiten des KLIPY-Zugangs.
einstellungSetzen() schrieb bis dahin IMMER Name UND Wert ins
Protokoll. Fuer "nachtruhe_an = 1" ist das genau richtig -- man
will sehen, wer wann was umgestellt hat. Fuer einen Schluessel
ist es das Gegenteil.
NACHGEMESSEN in der echten Datenbank, nicht vermutet:
kopie_schluessel (31.08.) steht VOLLSTAENDIG im Protokoll
vapid_paar (05.09.) wurde bei 120 Zeichen abgeschnitten,
kurz VOR dem privaten Teil
Dass der zweite heil blieb, lag an einer Laengengrenze, nicht an
einer Absicht. Das ist keine offene Tuer -- das Protokoll liegt
hinter nurAdmin --, aber der Wert wandert in JEDE Sicherung und in
jeden Export. Und der naechste Schluessel waere denselben Weg
gegangen.
ES IST EINE REGEL UND KEINE LISTE. Eine Liste
["kopie_schluessel","vapid_paar"] waere beim naechsten Geheimnis
still unvollstaendig -- derselbe Fehler wie bei den abgeschriebenen
Spaltennamen am 11.09. Wer eine Einstellung so BENENNT, meint auch
ein Geheimnis. Faellt ein harmloser Name versehentlich darunter,
ist der Schaden, dass im Protokoll etwas weniger Text steht.
WAS STEHEN BLEIBT: dass sich die Einstellung geaendert hat, wann,
und durch wen. Nur der Wert fehlt. Ein Protokolleintrag ohne
Eintrag waere die schlechtere Loesung -- dann saehe niemand mehr,
dass jemand den Schluessel ausgetauscht hat. */
const GEHEIME_EINSTELLUNG = /(schluessel|schlüssel|geheim|token|passwort|kennwort|secret|paar|(^|[_-])key([_-]|$))/i;
export function einstellungSetzen(schluessel, wert, akteur = null) {
db().prepare(`
INSERT INTO einstellungen (schluessel, wert, geaendert, von) VALUES (?,?,?,?)
ON CONFLICT(schluessel) DO UPDATE SET
wert = excluded.wert, geaendert = excluded.geaendert, von = excluded.von`).run(
schluessel, String(wert), new Date().toISOString(), akteur?.id ?? null);
const roh = String(wert ?? "");
const detail = GEHEIME_EINSTELLUNG.test(String(schluessel))
? `${schluessel} = ${roh ? "(gesetzt, " + roh.length + " Zeichen)" : "(geleert)"}`
: `${schluessel} = ${wert}`;
protokolliere("einstellung_geaendert", {
personId: akteur?.id ?? null, rolle: akteur?.rolle ?? null, ip: akteur?.ip ?? null,
detail: `${schluessel} = ${wert}`.slice(0, 120),
detail: detail.slice(0, 120),
});
}